goodmap 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodmap
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Map engine to serve all the people :)
5
5
  Author: Krzysztof Kolodzinski
6
6
  Author-email: krzysztof.kolodzinski@problematy.pl
@@ -16,6 +16,7 @@ Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
16
16
  Requires-Dist: Flask-WTF (>=1.2.1,<2.0.0)
17
17
  Requires-Dist: PyYAML (>=6.0,<7.0)
18
18
  Requires-Dist: aiohttp (>=3.8.4,<4.0.0)
19
+ Requires-Dist: deprecation (>=2.1.0,<3.0.0)
19
20
  Requires-Dist: flask-restx (>=1.3.0,<2.0.0)
20
21
  Requires-Dist: google-cloud-storage (>=2.7.0,<3.0.0)
21
22
  Requires-Dist: gql (>=3.4.0,<4.0.0)
@@ -34,6 +35,19 @@ Map engine to serve all the people ;)
34
35
 
35
36
  ## Setup
36
37
 
38
+ #### 0. Clone the repo
39
+ ```
40
+ git clone --recursive
41
+ ```
42
+ Remember, everytime you want to pull the newest changes, run:
43
+ ```
44
+ git pull
45
+ git submodule update
46
+ ```
47
+ because `goodmap` contains a submodule.
48
+
49
+ #TODO remove all submodule connected instructions after removing platzky submodule (see #157)
50
+
37
51
  #### 1. Use python 3.10
38
52
  If you have a different version of Python on your system, install python 3.10 alongside. For that, you can use [`pyenv`](https://github.com/pyenv/pyenv). Follow the [documentation](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation). Useful commands: `pyenv help <command>`, `pyenv install`, `pyenv shell`, `pyenv versions`.
39
53
 
@@ -96,7 +110,7 @@ TODO: `obligatory_fields` is a new subsection, start using it in the actual appl
96
110
  ```
97
111
  - `meta-data` - some special data like
98
112
  ```
99
- "UUID"
113
+ "uuid"
100
114
  ```
101
115
 
102
116
  You can define the fields in all these subsections. Besides these types of fields, there is no restriction on the number of fields a datapoint can have.
@@ -7,6 +7,19 @@ Map engine to serve all the people ;)
7
7
 
8
8
  ## Setup
9
9
 
10
+ #### 0. Clone the repo
11
+ ```
12
+ git clone --recursive
13
+ ```
14
+ Remember, everytime you want to pull the newest changes, run:
15
+ ```
16
+ git pull
17
+ git submodule update
18
+ ```
19
+ because `goodmap` contains a submodule.
20
+
21
+ #TODO remove all submodule connected instructions after removing platzky submodule (see #157)
22
+
10
23
  #### 1. Use python 3.10
11
24
  If you have a different version of Python on your system, install python 3.10 alongside. For that, you can use [`pyenv`](https://github.com/pyenv/pyenv). Follow the [documentation](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation). Useful commands: `pyenv help <command>`, `pyenv install`, `pyenv shell`, `pyenv versions`.
12
25
 
@@ -69,7 +82,7 @@ TODO: `obligatory_fields` is a new subsection, start using it in the actual appl
69
82
  ```
70
83
  - `meta-data` - some special data like
71
84
  ```
72
- "UUID"
85
+ "uuid"
73
86
  ```
74
87
 
75
88
  You can define the fields in all these subsections. Besides these types of fields, there is no restriction on the number of fields a datapoint can have.
@@ -1,3 +1,6 @@
1
+ # TODO move filtering to db site
2
+
3
+
1
4
  def does_fulfill_requirement(entry, requirements):
2
5
  matches = []
3
6
  for category, values in requirements:
@@ -1,14 +1,14 @@
1
1
  import importlib.metadata
2
+ import uuid
2
3
 
4
+ import deprecation
3
5
  from flask import Blueprint, jsonify, make_response, request
4
6
  from flask_babel import gettext
5
7
  from flask_restx import Api, Resource, fields
6
8
  from platzky.config import LanguagesMapping
7
9
 
8
- from goodmap.data_models.location import Location
9
-
10
- from .core import get_queried_data
11
- from .formatter import prepare_pin
10
+ from goodmap.core import get_queried_data
11
+ from goodmap.formatter import prepare_pin
12
12
 
13
13
 
14
14
  def make_tuple_translation(keys_to_translate):
@@ -16,7 +16,7 @@ def make_tuple_translation(keys_to_translate):
16
16
 
17
17
 
18
18
  def core_pages(
19
- database, languages: LanguagesMapping, notifier_function, csrf_generator
19
+ database, languages: LanguagesMapping, notifier_function, csrf_generator, location_model
20
20
  ) -> Blueprint:
21
21
  core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
22
22
  core_api = Api(core_api_blueprint, doc="/doc", version="0.1")
@@ -29,26 +29,28 @@ def core_pages(
29
29
  },
30
30
  )
31
31
 
32
- location_suggest_model = core_api.model(
32
+ # TODO get this from Location pydantic model
33
+ suggested_location_model = core_api.model(
33
34
  "LocationSuggestion",
34
35
  {
35
- "name": fields.String(required=True, description="Organization name"),
36
- "coordinates": fields.String(required=True, description="Location of the suggestion"),
36
+ "name": fields.String(required=False, description="Organization name"),
37
+ "position": fields.String(required=True, description="Location of the suggestion"),
37
38
  "photo": fields.String(required=False, description="Photo of the location"),
38
39
  },
39
40
  )
40
41
 
41
42
  @core_api.route("/suggest-new-point")
42
43
  class NewLocation(Resource):
43
- @core_api.expect(location_suggest_model)
44
+ @core_api.expect(suggested_location_model)
44
45
  def post(self):
45
46
  """Suggest new location"""
46
47
  try:
47
- location_suggest = request.get_json()
48
- location = Location.model_validate(location_suggest)
48
+ suggested_location = request.get_json()
49
+ suggested_location.update({"uuid": str(uuid.uuid4())})
50
+ location = location_model.model_validate(suggested_location)
49
51
  message = (
50
- f"A new location has been suggested: '{location.name}' "
51
- f"at position: {location.coordinates}"
52
+ f"A new location has been suggested under uuid: '{location.uuid}' "
53
+ f"at position: {location.position}"
52
54
  )
53
55
  notifier_function(message)
54
56
  except ValueError as e:
@@ -75,6 +77,12 @@ def core_pages(
75
77
 
76
78
  @core_api.route("/data")
77
79
  class Data(Resource):
80
+ @deprecation.deprecated(
81
+ deprecated_in="0.4.1",
82
+ removed_in="0.5.0",
83
+ current_version=importlib.metadata.version("goodmap"),
84
+ details="Use /locations or /location/<point_id> instead",
85
+ )
78
86
  def get(self):
79
87
  """
80
88
  Shows all data filtered by query parameters
@@ -90,6 +98,34 @@ def core_pages(
90
98
  formatted_data = [prepare_pin(x, visible_data, meta_data) for x in queried_data]
91
99
  return jsonify(formatted_data)
92
100
 
101
+ @core_api.route("/locations")
102
+ class GetLocations(Resource):
103
+ def get(self):
104
+ """
105
+ Shows list of locations with uuid and position
106
+ """
107
+ query_params = request.args.to_dict(flat=False)
108
+ all_locations = database.get_locations(query_params)
109
+ return jsonify([x.basic_info() for x in all_locations])
110
+
111
+ @core_api.route("/location/<location_id>")
112
+ class GetLocation(Resource):
113
+ def get(self, location_id):
114
+ """
115
+ Shows a single location with all data
116
+ """
117
+ location = database.get_location(location_id)
118
+
119
+ # TODO getting visible_data and meta_data should be taken from db methods
120
+ # e.g. db.get_visible_data() and db.get_meta_data()
121
+ # visible_data and meta_data should be models
122
+ all_data = database.get_data()
123
+ visible_data = all_data["visible_data"]
124
+ meta_data = all_data["meta_data"]
125
+
126
+ formatted_data = prepare_pin(location.model_dump(), visible_data, meta_data)
127
+ return jsonify(formatted_data)
128
+
93
129
  @core_api.route("/version")
94
130
  class Version(Resource):
95
131
  def get(self):
@@ -0,0 +1,37 @@
1
+ from typing import Any, Type
2
+
3
+ from pydantic import BaseModel, Field, create_model, field_validator
4
+
5
+
6
+ class LocationBase(BaseModel, extra="allow"):
7
+ position: tuple[float, float]
8
+ uuid: str
9
+
10
+ @field_validator("position")
11
+ @classmethod
12
+ def position_must_be_valid(cls, v):
13
+ if v[0] < -90 or v[0] > 90:
14
+ raise ValueError("latitude must be in range -90 to 90")
15
+ if v[1] < -180 or v[1] > 180:
16
+ raise ValueError("longitude must be in range -180 to 180")
17
+ return v
18
+
19
+ def basic_info(self):
20
+ return {"uuid": self.uuid, "position": self.position}
21
+
22
+
23
+ def create_location_model(obligatory_fields: list[tuple[str, Type[Any]]]) -> Type[BaseModel]:
24
+ fields = {
25
+ field_name: (field_type, Field(...)) for (field_name, field_type) in obligatory_fields
26
+ }
27
+
28
+ return create_model(
29
+ "Location",
30
+ __config__=None,
31
+ __doc__=None,
32
+ __module__="Location",
33
+ __validators__=None,
34
+ __base__=LocationBase,
35
+ __cls_kwargs__=None,
36
+ **fields,
37
+ )
@@ -76,7 +76,7 @@ def report_data_violations_from_json(json_database):
76
76
  map_data = json_database["map"]
77
77
  datapoints = map_data["data"]
78
78
  categories = map_data["categories"]
79
- obligatory_fields = map_data["obligatory_fields"]
79
+ obligatory_fields = map_data["location_obligatory_fields"]
80
80
 
81
81
  data_violations = []
82
82
 
@@ -0,0 +1,116 @@
1
+ import json
2
+ from functools import partial
3
+
4
+ from goodmap.core import get_queried_data
5
+
6
+ # TODO file is temporary solution to be compatible with old, static code,
7
+ # it should be replaced with dynamic solution
8
+
9
+
10
+ # ------------------------------------------------
11
+ # get_location_obligatory_fields
12
+
13
+
14
+ def json_db_get_location_obligatory_fields(db):
15
+ return db.data["location_obligatory_fields"]
16
+
17
+
18
+ def json_file_db_get_location_obligatory_fields(db):
19
+ with open(db.data_file_path, "r") as file:
20
+ return json.load(file)["map"]["location_obligatory_fields"]
21
+
22
+
23
+ def google_json_db_get_location_obligatory_fields(db):
24
+ return json.loads(db.blob.download_as_text(client=None))["map"]["location_obligatory_fields"]
25
+
26
+
27
+ def get_location_obligatory_fields(db):
28
+ return globals()[f"{db.module_name}_get_location_obligatory_fields"](db)
29
+
30
+
31
+ # ------------------------------------------------
32
+ # get_data
33
+ def google_json_db_get_data(self):
34
+ return json.loads(self.blob.download_as_text(client=None))["map"]
35
+
36
+
37
+ def json_file_db_get_data(self):
38
+ with open(self.data_file_path, "r") as file:
39
+ return json.load(file)["map"]
40
+
41
+
42
+ def json_db_get_data(self):
43
+ return self.data
44
+
45
+
46
+ def get_data(db):
47
+ return globals()[f"{db.module_name}_get_data"]
48
+
49
+
50
+ # ------------------------------------------------
51
+ # get_location
52
+
53
+
54
+ def get_location_from_raw_data(raw_data, uuid, location_model):
55
+ point = next((point for point in raw_data["data"] if point["uuid"] == uuid), None)
56
+ return location_model.model_validate(point) if point else None
57
+
58
+
59
+ def google_json_db_get_location(self, uuid, location_model):
60
+ return get_location_from_raw_data(
61
+ json.loads(self.blob.download_as_text(client=None))["map"], uuid, location_model
62
+ )
63
+
64
+
65
+ def json_file_db_get_location(self, uuid, location_model):
66
+ with open(self.data_file_path, "r") as file:
67
+ point = get_location_from_raw_data(json.load(file)["map"], uuid, location_model)
68
+ return point
69
+
70
+
71
+ def json_db_get_location(self, uuid, location_model):
72
+ return get_location_from_raw_data(self.data, uuid, location_model)
73
+
74
+
75
+ def get_location(db, location_model):
76
+ return partial(globals()[f"{db.module_name}_get_location"], location_model=location_model)
77
+
78
+
79
+ # ------------------------------------------------
80
+ # get_locations
81
+
82
+
83
+ def get_locations_list_from_raw_data(map_data, query, location_model):
84
+ filtered_locations = get_queried_data(map_data["data"], map_data["categories"], query)
85
+ return [location_model.model_validate(point) for point in filtered_locations]
86
+
87
+
88
+ def google_json_db_get_locations(self, query, location_model):
89
+ return get_locations_list_from_raw_data(
90
+ json.loads(self.blob.download_as_text(client=None))["map"], query, location_model
91
+ )
92
+
93
+
94
+ def json_file_db_get_locations(self, query, location_model):
95
+ with open(self.data_file_path, "r") as file:
96
+ return get_locations_list_from_raw_data(json.load(file)["map"], query, location_model)
97
+
98
+
99
+ def json_db_get_locations(self, query, location_model):
100
+ return get_locations_list_from_raw_data(self.data, query, location_model)
101
+
102
+
103
+ def get_locations(db, location_model):
104
+ return partial(globals()[f"{db.module_name}_get_locations"], location_model=location_model)
105
+
106
+
107
+ # TODO extension function should be replaced with simple extend which would take a db plugin
108
+ # it could look like that:
109
+ # `db.extend(goodmap_db_plugin)` in plugin all those functions would be organized
110
+
111
+
112
+ def extend_db_with_goodmap_queries(db, location_model):
113
+ db.extend("get_data", get_data(db))
114
+ db.extend("get_locations", get_locations(db, location_model))
115
+ db.extend("get_location", get_location(db, location_model))
116
+ return db
@@ -0,0 +1,52 @@
1
+ import os
2
+
3
+ from flask import Blueprint, render_template
4
+ from flask_wtf.csrf import CSRFProtect, generate_csrf
5
+ from platzky import platzky
6
+ from platzky.config import Config, languages_dict
7
+
8
+ from goodmap.core_api import core_pages
9
+ from goodmap.data_models.location import create_location_model
10
+ from goodmap.db import extend_db_with_goodmap_queries, get_location_obligatory_fields
11
+
12
+
13
+ def create_app(config_path: str) -> platzky.Engine:
14
+ config = Config.parse_yaml(config_path)
15
+ return create_app_from_config(config)
16
+
17
+
18
+ # TODO Checking if there is a feature flag secition should be part of configs logic not client app
19
+ def is_feature_enabled(config: Config, feature: str) -> bool:
20
+ return config.feature_flags.get(feature, False) if config.feature_flags else False
21
+
22
+
23
+ def create_app_from_config(config: Config) -> platzky.Engine:
24
+ directory = os.path.dirname(os.path.realpath(__file__))
25
+
26
+ locale_dir = os.path.join(directory, "locale")
27
+ config.translation_directories.append(locale_dir)
28
+ app = platzky.create_app_from_config(config)
29
+
30
+ if is_feature_enabled(config, "USE_LAZY_LOADING"):
31
+ location_obligatory_fields = get_location_obligatory_fields(app.db)
32
+ else:
33
+ location_obligatory_fields = []
34
+
35
+ location_model = create_location_model(location_obligatory_fields)
36
+
37
+ app.db = extend_db_with_goodmap_queries(app.db, location_model)
38
+
39
+ CSRFProtect(app)
40
+
41
+ cp = core_pages(
42
+ app.db, languages_dict(config.languages), app.notify, generate_csrf, location_model
43
+ )
44
+ app.register_blueprint(cp)
45
+ goodmap = Blueprint("goodmap", __name__, url_prefix="/", template_folder="templates")
46
+
47
+ @goodmap.route("/")
48
+ def index():
49
+ return render_template("map.html", feature_flags=config.feature_flags)
50
+
51
+ app.register_blueprint(goodmap)
52
+ return app
@@ -35,12 +35,11 @@
35
35
  </style>
36
36
 
37
37
  <div id="loadingPopup" class="loading-popup">
38
- Loading...
38
+ {{ gettext('Loading') }}...
39
39
  </div>
40
40
 
41
41
  <div id="map" class="map h-100 w-100"></div>
42
42
 
43
-
44
43
  <script type="text/javascript">
45
44
 
46
45
  (function() {
@@ -113,6 +112,10 @@ function hideLoading() {
113
112
  window.APP_LANG = "{{ current_language }}";
114
113
  window.SECONDARY_COLOR = "{{ secondary_color }}";
115
114
  window.PRIMARY_COLOR = "{{ primary_color }}";
115
+
116
+ window.SHOW_SUGGEST_NEW_POINT_BUTTON = {{ feature_flags.SHOW_SUGGEST_NEW_POINT_BUTTON | default(false) | tojson }};
117
+ window.SHOW_SEARCH_BAR = {{ feature_flags.SHOW_SEARCH_BAR | default(false) | tojson }};
118
+ window.USE_LAZY_LOADING = {{ feature_flags.USE_LAZY_LOADING | default(false) | tojson }};
116
119
  </script>
117
120
  <script src="/static/map.js"></script>
118
121
  {% endblock %}
@@ -1,9 +1,10 @@
1
1
  [tool.poetry]
2
2
  name = "goodmap"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "Map engine to serve all the people :)"
5
5
  authors = ["Krzysztof Kolodzinski <krzysztof.kolodzinski@problematy.pl>"]
6
6
  readme = "README.md"
7
+ exclude = ["goodmap/locale/*/LC_MESSAGES/*.po"]
7
8
 
8
9
  [tool.poetry.dependencies]
9
10
  python = "^3.10"
@@ -20,6 +21,7 @@ gql = "^3.4.0"
20
21
  aiohttp = "^3.8.4"
21
22
  pydantic = "^2.7.1"
22
23
  platzky = "^0.2.5"
24
+ deprecation = "^2.1.0"
23
25
 
24
26
  [tool.poetry.group.dev.dependencies]
25
27
  pytest = "^7.1.2"
@@ -1,15 +0,0 @@
1
- from pydantic import BaseModel, field_validator
2
-
3
-
4
- class Location(BaseModel):
5
- name: str
6
- coordinates: tuple[float, float]
7
-
8
- @field_validator("coordinates")
9
- @classmethod
10
- def coordinates_must_be_valid(cls, v):
11
- if v[0] < -90 or v[0] > 90:
12
- raise ValueError("latitude must be in range -90 to 90")
13
- if v[1] < -180 or v[1] > 180:
14
- raise ValueError("longitude must be in range -180 to 180")
15
- return v
@@ -1,23 +0,0 @@
1
- import json
2
-
3
- # TODO file is temporary solution to be compatible with old, static code,
4
- # it should be replaced with dynamic solution
5
-
6
-
7
- def google_json_db_get_data(self):
8
- raw_data = self.blob.download_as_text(client=None)
9
- return json.loads(raw_data)["map"]
10
-
11
-
12
- def json_file_db_get_data(self):
13
- with open(self.data_file_path, "r") as file:
14
- return json.load(file)["map"]
15
-
16
-
17
- def json_db_get_data(self):
18
- return self.data
19
-
20
-
21
- def get_data(db):
22
- function_name = f"{db.module_name}_get_data"
23
- return globals()[function_name]
@@ -1,30 +0,0 @@
1
- from flask import Blueprint, render_template
2
- from flask_wtf.csrf import CSRFProtect, generate_csrf
3
- from platzky import platzky
4
- from platzky.config import Config, languages_dict
5
-
6
- from .core_api import core_pages
7
- from .db import get_data
8
-
9
-
10
- def create_app(config_path: str) -> platzky.Engine:
11
- config = Config.parse_yaml(config_path)
12
- return create_app_from_config(config)
13
-
14
-
15
- def create_app_from_config(config: Config) -> platzky.Engine:
16
- app = platzky.create_app_from_config(config)
17
-
18
- app.db.extend("get_data", get_data(app.db))
19
- CSRFProtect(app)
20
-
21
- cp = core_pages(app.db, languages_dict(config.languages), app.notify, generate_csrf)
22
- app.register_blueprint(cp)
23
- goodmap = Blueprint("goodmap", __name__, url_prefix="/", template_folder="templates")
24
-
25
- @goodmap.route("/")
26
- def index():
27
- return render_template("map.html")
28
-
29
- app.register_blueprint(goodmap)
30
- return app
File without changes
File without changes
File without changes