Skip to content

Commit 57c8634

Browse files
committed
initial commit
0 parents  commit 57c8634

15 files changed

+438
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
venv/
2+
.idea/
3+
__pycache__
4+
flaskr/__pycache__
5+
migrations/__pycache__
6+

Dockerfile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM ubuntu:18.04
2+
3+
RUN apt-get update -y && \
4+
apt-get install -y python3-pip python3-dev python3
5+
6+
# We copy just the requirements.txt first to leverage Docker cache
7+
COPY ./requirements.txt /app/requirements.txt
8+
9+
WORKDIR /app
10+
11+
RUN pip3 install -r requirements.txt
12+
13+
COPY . /app
14+
15+
ENV FLASK_APP=run.py
16+
ENV FLASK_DEBUG=True
17+
ENV LC_ALL=C.UTF-8
18+
ENV LANG=C.UTF-8
19+
20+
#ENTRYPOINT "/usr/bin/python3"
21+
22+
CMD [ "flask", "run", "--host=0.0.0.0" ]

README.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
### Installation
2+
clone the repository
3+
4+
cd python-flask-elastic
5+
docker-compose up -d
6+
7+
Run the migrations for elasticsearch
8+
9+
cd migrations
10+
python3 migration_runner.py
11+
12+
### Usage
13+
To add a pokemon to the database:
14+
```
15+
curl --header "Content-Type: application/json" -d "{
16+
\"pokadex_id\": 25,
17+
\"name\": \"Stam\",
18+
\"nickname\": \"Lior Ha Gever\",
19+
\"level\": 60,
20+
\"type\": \"ELECTRIC\",
21+
\"skills\": [
22+
\"Tail Whip\"
23+
]
24+
}" localhost:5000/new_pokemon
25+
```
26+
To search (autocomplete) for a pokemon, browse to
27+
http://localhost:5000/autocomplete/<search_term>
28+
### Requirements
29+
* docker-compose
30+
* python3.6+
31+
32+
```
33+
34+
35+
curl --header "Content-Type: application/json" -d "{
36+
\"pokadex_id\": 26,
37+
\"name\": \"Pikachu\",
38+
\"nickname\": \"Baruh Ha Gever\",
39+
\"level\": 60,
40+
\"type\": \"ELECTRIC\",
41+
\"skills\": [
42+
\"Tail Whip\"
43+
]
44+
}"
45+
```
46+
47+
### Notes
48+
The migration script should wait until the elasticsearch
49+
warms up (estimated: 25 seconds)

configs/app_config.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PORT: 5000
2+
ES_CONNECTION:
3+
HOST: elasticsearch1
4+
PORT: 9200

configs/config.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import yaml
2+
3+
4+
def load_config(path: str):
5+
with open(path, 'r') as f:
6+
cfg = yaml.safe_load(f)
7+
return cfg

docker-compose.yaml

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
version: '3.7'
3+
services:
4+
elasticsearch:
5+
image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0
6+
container_name: elasticsearch1
7+
environment:
8+
- node.name=elasticsearch1
9+
- cluster.name=docker-cluster
10+
- cluster.initial_master_nodes=elasticsearch1
11+
- bootstrap.memory_lock=true
12+
- "ES_JAVA_OPTS=-Xms1024M -Xmx1024M"
13+
- http.cors.enabled=true
14+
- http.cors.allow-origin=*
15+
- network.host=_eth0_
16+
healthcheck:
17+
test: ["CMD", "curl", "-f", "http://localhost:9200"]
18+
interval: 1m30s
19+
timeout: 10s
20+
retries: 3
21+
start_period: 40s
22+
ulimits:
23+
nproc: 65535
24+
memlock:
25+
soft: -1
26+
hard: -1
27+
cap_add:
28+
- ALL
29+
# privileged: true
30+
deploy:
31+
replicas: 1
32+
update_config:
33+
parallelism: 1
34+
delay: 10s
35+
resources:
36+
limits:
37+
cpus: '1'
38+
memory: 256M
39+
reservations:
40+
cpus: '1'
41+
memory: 256M
42+
restart_policy:
43+
condition: on-failure
44+
delay: 5s
45+
max_attempts: 3
46+
window: 10s
47+
volumes:
48+
- type: volume
49+
source: logs
50+
target: /var/log
51+
- type: volume
52+
source: esdata3
53+
target: /usr/share/elasticsearch/data
54+
networks:
55+
esnet:
56+
aliases:
57+
- elasticsearch
58+
ports:
59+
- 9200:9200
60+
- 9300:9300
61+
migrate:
62+
image: webapp-python
63+
depends_on:
64+
- elasticsearch
65+
networks:
66+
- esnet
67+
working_dir: /app/migrations
68+
command: [ "python3", "migration_runner.py" ]
69+
flask:
70+
image: webapp-python
71+
depends_on:
72+
- elasticsearch
73+
networks:
74+
- esnet
75+
build:
76+
context: .
77+
dockerfile: Dockerfile
78+
volumes:
79+
- .:/app
80+
environment:
81+
- FLASK_APP=/app/run.py
82+
- FLASK_DEBUG=True
83+
- LC_ALL=C.UTF-8
84+
- LANG=C.UTF-8
85+
ports:
86+
- 5000:5000
87+
command: [ "flask", "run", "--host=0.0.0.0" ]
88+
volumes:
89+
esdata3:
90+
logs:
91+
92+
networks:
93+
esnet:
94+
driver: bridge

flaskr/routes.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from run import app, es
2+
from flask import request, jsonify
3+
import flaskr.utils as utils
4+
5+
POKEMON_INDEX = 'pokemon'
6+
7+
@app.route('/', methods=['GET'])
8+
def index():
9+
return 'This is the index page\n'
10+
11+
12+
@app.route('/new_pokemon', methods=['POST'])
13+
def add_pokemon():
14+
if not utils.valid_new_pokemon_schema(request.json):
15+
return 'Bad Request\n'
16+
pokemon_id, pokemon_body = utils.valid_pokemon_dict_to_id_body(request.json)
17+
result = es.index(index=POKEMON_INDEX, id=pokemon_id, body=pokemon_body)
18+
return f'New Pokemon Added\n{jsonify(result)}\n'
19+
20+
21+
@app.route('/autocomplete/<string:pokemon>')
22+
def auto_complete(pokemon):
23+
fields = ['nickname', 'name', 'skills']
24+
results = es.search(index=POKEMON_INDEX,
25+
body={'query': {'multi_match': {'fields': fields, 'query': pokemon, }}})
26+
return jsonify(results['hits']['hits'])
27+
28+
29+
@app.route('/query_pokemon', methods=['POST'])
30+
def query():
31+
results = es.get(index=POKEMON_INDEX, id=int(request.json.get('id', 0)))
32+
return jsonify(results['_source'])

flaskr/utils.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from cerberus import validator
2+
3+
_VALID_POKEMON_LEVELS = [10 * x for x in range(10)]
4+
5+
6+
def _valid_level(field, value, error):
7+
if value not in _VALID_POKEMON_LEVELS:
8+
error(field, "Invalid Pokemon Level")
9+
10+
11+
_VALID_POKEMON_TYPES = 'ELECTRIC GROUND FIRE WATER WIND PSYCHIC GRASS'.split()
12+
13+
14+
def _valid_type(field, value, error):
15+
if value not in _VALID_POKEMON_TYPES:
16+
error(field, "Invalid Pokemon Type")
17+
18+
19+
_NEW_POKEMON_SCHEMA = {'pokadex_id': {'required': True, 'type': 'integer'},
20+
'name': {'required': True, 'type': 'string'},
21+
'nickname': {'required': True, 'type': 'string'},
22+
'level': {'required': True, 'check_with': _valid_level},
23+
'type': {'required': True, 'check_with': _valid_type},
24+
'skills': {'required': True, 'type': 'list', 'schema': {'type': 'string'}}
25+
}
26+
27+
_POKEMON_INDEX_SCHEMA = {
28+
'settings': {
29+
"analysis": {
30+
"filter": {
31+
"autocomplete_filter": {
32+
"type": "edge_ngram",
33+
"min_gram": 1,
34+
"max_gram": 20
35+
}
36+
},
37+
"analyzer": {
38+
"autocomplete": {
39+
"type": "custom",
40+
"tokenizer": "standard",
41+
"filter": [
42+
"lowercase",
43+
"autocomplete_filter"
44+
]
45+
}
46+
}
47+
}
48+
},
49+
'mappings': {
50+
'properties': {
51+
'pokadex_id': {'type': 'integer'},
52+
'name': {'type': 'completion', 'analyzer': 'autocomplete'},
53+
'nickname': {'type': 'completion', 'analyzer': 'autocomplete'},
54+
'level': {'type': 'integer'},
55+
'type': {'type': 'text'},
56+
'skills': {'type': 'text'}
57+
}
58+
}
59+
}
60+
61+
62+
def valid_new_pokemon_schema(dictionary):
63+
val = validator.Validator(_NEW_POKEMON_SCHEMA)
64+
return val.validate(dictionary)
65+
66+
67+
def valid_pokemon_dict_to_id_body(dictionary):
68+
pokemon_id = dictionary.get('pokadex_id')
69+
return int(pokemon_id), dictionary
70+
71+
72+
def generate_index(elastic_obj):
73+
if elastic_obj.indices.exists(index='pokemon'):
74+
elastic_obj.indices.delete(index='pokemon')
75+
elastic_obj.indices.create(index='pokemon', body=_POKEMON_INDEX_SCHEMA)

lib/models/pokemon.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from cerberus import validator
2+
3+
_VALID_POKEMON_LEVELS = [10 * x for x in range(10)]
4+
5+
6+
def _valid_level(field, value, error):
7+
if value not in _VALID_POKEMON_LEVELS:
8+
error(field, "Invalid Pokemon Level")
9+
10+
11+
_VALID_POKEMON_TYPES = 'ELECTRIC GROUND FIRE WATER WIND PSYCHIC GRASS'.split()
12+
13+
14+
def _valid_type(field, value, error):
15+
if value not in _VALID_POKEMON_TYPES:
16+
error(field, "Invalid Pokemon Type")
17+
18+
19+
_NEW_POKEMON_SCHEMA = {'pokadex_id': {'required': True, 'type': 'integer'},
20+
'name': {'required': True, 'type': 'string'},
21+
'nickname': {'required': True, 'type': 'string'},
22+
'level': {'required': True, 'check_with': _valid_level},
23+
'type': {'required': True, 'check_with': _valid_type},
24+
'skills': {'required': True, 'type': 'list', 'schema': {'type': 'string'}}
25+
}
26+
27+
28+
def valid_new_pokemon_schema(dictionary):
29+
val = validator.Validator(_NEW_POKEMON_SCHEMA)
30+
return val.validate(dictionary)
31+
32+
33+
def valid_pokemon_dict_to_id_body(dictionary):
34+
pokemon_id = dictionary.get('pokadex_id')
35+
return int(pokemon_id), dictionary
36+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from es_migration_base import BaseESMigration
2+
3+
4+
class Migration(BaseESMigration):
5+
6+
def __init__(self, es_object):
7+
super().__init__(es_object=es_object, es_index='pokemon')
8+
self.schema = {
9+
'settings': {
10+
"analysis": {
11+
"filter": {
12+
"autocomplete_filter": {
13+
"type": "edge_ngram",
14+
"min_gram": 1,
15+
"max_gram": 20
16+
}
17+
},
18+
"analyzer": {
19+
"autocomplete": {
20+
"type": "custom",
21+
"tokenizer": "standard",
22+
"filter": [
23+
"lowercase",
24+
"autocomplete_filter"
25+
]
26+
}
27+
}
28+
}
29+
},
30+
'mappings': {
31+
'properties': {
32+
'pokadex_id': {'type': 'integer'},
33+
'name': {'type': 'completion', 'analyzer': 'autocomplete'},
34+
'nickname': {'type': 'completion', 'analyzer': 'autocomplete'},
35+
'level': {'type': 'integer'},
36+
'type': {'type': 'text'},
37+
'skills': {'type': 'completion', 'analyzer': 'autocomplete'}
38+
}
39+
}
40+
}
41+
42+
def execute(self):
43+
if self._es_object.indices.exists(index='pokemon'):
44+
self._es_object.indices.delete(index='pokemon')
45+
self._es_object.indices.create(index='pokemon', body=self.schema)

migrations/__init__.py

Whitespace-only changes.

migrations/es_migration_base.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from abc import ABC, abstractmethod
2+
3+
4+
class BaseESMigration(ABC):
5+
def __init__(self, es_object, es_index):
6+
self._es_object = es_object
7+
self.es_index = es_index
8+
9+
@abstractmethod
10+
def execute(self):
11+
pass

0 commit comments

Comments
 (0)