goodmap 1.3.1__py3-none-any.whl → 1.5.0__py3-none-any.whl

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.
goodmap/admin_api.py CHANGED
@@ -34,7 +34,9 @@ def _clean_model_name(model: Type[Any]) -> str:
34
34
  def _handle_location_validation_error(e: LocationValidationError):
35
35
  """Handle LocationValidationError and return appropriate response."""
36
36
  logger.warning(
37
- "Location validation failed",
37
+ "Location validation failed - uuid: %s, errors: %s",
38
+ e.uuid,
39
+ e.validation_errors,
38
40
  extra={"uuid": e.uuid, "errors": e.validation_errors},
39
41
  )
40
42
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
@@ -126,7 +128,9 @@ def _update_suggestion_handler(database, suggestion_id):
126
128
  database.update_suggestion(suggestion_id, status)
127
129
  except LocationValidationError as e:
128
130
  logger.warning(
129
- "Location validation failed in suggestion",
131
+ "Location validation failed in suggestion - uuid: %s, errors: %s",
132
+ e.uuid,
133
+ e.validation_errors,
130
134
  extra={"uuid": e.uuid, "errors": e.validation_errors},
131
135
  )
132
136
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
goodmap/core_api.py CHANGED
@@ -7,7 +7,9 @@ import numpy
7
7
  import pysupercluster
8
8
  from flask import Blueprint, jsonify, make_response, request
9
9
  from flask_babel import gettext
10
- from platzky.config import LanguagesMapping
10
+ from platzky import FeatureFlagSet
11
+ from platzky.attachment import AttachmentProtocol
12
+ from platzky.config import AttachmentConfig, LanguagesMapping
11
13
  from spectree import Response, SpecTree
12
14
 
13
15
  from goodmap.api_models import (
@@ -23,6 +25,7 @@ from goodmap.clustering import (
23
25
  match_clusters_uuids,
24
26
  )
25
27
  from goodmap.exceptions import LocationValidationError
28
+ from goodmap.feature_flags import CategoriesHelp
26
29
  from goodmap.formatter import prepare_pin
27
30
  from goodmap.json_security import (
28
31
  MAX_JSON_DEPTH_LOCATION,
@@ -80,15 +83,21 @@ def core_pages(
80
83
  notifier_function,
81
84
  csrf_generator,
82
85
  location_model,
83
- feature_flags={},
86
+ photo_attachment_class: type[AttachmentProtocol],
87
+ photo_attachment_config: AttachmentConfig,
88
+ feature_flags: FeatureFlagSet,
84
89
  ) -> Blueprint:
85
90
  core_api_blueprint = Blueprint("api", __name__, url_prefix="/api")
86
91
 
87
- # Initialize Spectree for API documentation and validation
88
- # Use simple naming strategy without hashes for cleaner schema names
89
- from typing import Any, Type
92
+ # Build photo error message from config
93
+ allowed_ext = ", ".join(sorted(photo_attachment_config.allowed_extensions or []))
94
+ max_size_mb = photo_attachment_config.max_size / (1024 * 1024)
95
+ error_invalid_photo = (
96
+ f"Invalid photo. Allowed formats: {allowed_ext}. Max size: {max_size_mb:.0f}MB."
97
+ )
90
98
 
91
- def _clean_model_name(model: Type[Any]) -> str:
99
+ # Initialize Spectree for API documentation and validation
100
+ def _clean_model_name(model: type) -> str:
92
101
  return model.__name__
93
102
 
94
103
  spec = SpecTree(
@@ -111,6 +120,9 @@ def core_pages(
111
120
  import json as json_lib
112
121
 
113
122
  try:
123
+ # Initialize photo attachment (only populated for multipart/form-data)
124
+ photo_attachment = None
125
+
114
126
  # Handle both multipart/form-data (with file uploads) and JSON
115
127
  if request.content_type and request.content_type.startswith("multipart/form-data"):
116
128
  # Parse form data dynamically
@@ -146,8 +158,24 @@ def core_pages(
146
158
  # If not JSON, use as-is (simple string values)
147
159
  suggested_location[key] = value
148
160
 
149
- # TODO: Handle photo file upload from request.files['photo']
150
- # For now, we just ignore it as the backend doesn't store photos yet
161
+ # Extract and validate photo attachment if present
162
+ photo_file = request.files.get("photo")
163
+ if photo_file and photo_file.filename:
164
+ photo_content = photo_file.read()
165
+ photo_mime = photo_file.content_type or "application/octet-stream"
166
+
167
+ # Validate using configured Attachment class
168
+ try:
169
+ photo_attachment = photo_attachment_class(
170
+ photo_file.filename, photo_content, photo_mime
171
+ )
172
+ except ValueError as e:
173
+ logger.warning(
174
+ "Rejected photo: %s",
175
+ e,
176
+ extra={"photo_filename": photo_file.filename},
177
+ )
178
+ return make_response(jsonify({"message": error_invalid_photo}), 400)
151
179
  else:
152
180
  # Parse JSON data with security checks (depth/size protection)
153
181
  raw_data = request.get_data(as_text=True)
@@ -186,10 +214,18 @@ def core_pages(
186
214
  database.add_suggestion(location.model_dump())
187
215
  message = gettext("A new location has been suggested with details")
188
216
  notifier_message = f"{message}: {json_lib.dumps(suggested_location, indent=2)}"
189
- notifier_function(notifier_message)
217
+ attachments = [photo_attachment] if photo_attachment else None
218
+ notifier_function(notifier_message, attachments=attachments)
190
219
  except LocationValidationError as e:
220
+ # NOTE: validation_errors includes input values from the location model fields:
221
+ # - Core fields: position (lat/long), uuid, remark
222
+ # - Dynamic fields: categories and obligatory_fields configured per deployment
223
+ # These are geographic/categorical data, NOT PII (no email, phone, names of people).
224
+ # Safe to log for debugging. If PII fields are ever added to the location model,
225
+ # strip 'input' from validation_errors before logging.
191
226
  logger.warning(
192
- "Location validation failed in suggest endpoint",
227
+ "Location validation failed in suggest endpoint: %s",
228
+ e.validation_errors,
193
229
  extra={"errors": e.validation_errors},
194
230
  )
195
231
  return make_response(jsonify({"message": ERROR_INVALID_LOCATION_DATA}), 400)
@@ -349,15 +385,15 @@ def core_pages(
349
385
  raw_categories = database.get_categories()
350
386
  categories = make_tuple_translation(raw_categories)
351
387
 
352
- if not feature_flags.get("CATEGORIES_HELP", False):
388
+ if CategoriesHelp not in feature_flags:
353
389
  return jsonify(categories)
354
- else:
355
- category_data = database.get_category_data()
356
- categories_help = category_data.get("categories_help")
357
- proper_categories_help = []
358
- if categories_help is not None:
359
- for option in categories_help:
360
- proper_categories_help.append({option: gettext(f"categories_help_{option}")})
390
+
391
+ category_data = database.get_category_data()
392
+ categories_help = category_data.get("categories_help")
393
+ proper_categories_help = []
394
+ if categories_help is not None:
395
+ for option in categories_help:
396
+ proper_categories_help.append({option: gettext(f"categories_help_{option}")})
361
397
 
362
398
  return jsonify({"categories": categories, "categories_help": proper_categories_help})
363
399
 
@@ -381,7 +417,7 @@ def core_pages(
381
417
  "options": make_tuple_translation(options),
382
418
  }
383
419
 
384
- if feature_flags.get("CATEGORIES_HELP", False):
420
+ if CategoriesHelp in feature_flags:
385
421
  option_help_list = categories_options_help.get(key, [])
386
422
  proper_options_help = []
387
423
  for option in option_help_list:
@@ -394,7 +430,7 @@ def core_pages(
394
430
 
395
431
  response = {"categories": result}
396
432
 
397
- if feature_flags.get("CATEGORIES_HELP", False):
433
+ if CategoriesHelp in feature_flags:
398
434
  categories_help = categories_data.get("categories_help", [])
399
435
  proper_categories_help = []
400
436
  for option in categories_help:
@@ -432,15 +468,15 @@ def core_pages(
432
468
  proper_categories_options_help.append(
433
469
  {option: gettext(f"categories_options_help_{option}")}
434
470
  )
435
- if not feature_flags.get("CATEGORIES_HELP", False):
471
+ if CategoriesHelp not in feature_flags:
436
472
  return jsonify(local_data)
437
- else:
438
- return jsonify(
439
- {
440
- "categories_options": local_data,
441
- "categories_options_help": proper_categories_options_help,
442
- }
443
- )
473
+
474
+ return jsonify(
475
+ {
476
+ "categories_options": local_data,
477
+ "categories_options_help": proper_categories_options_help,
478
+ }
479
+ )
444
480
 
445
481
  # Register Spectree with blueprint after all routes are defined
446
482
  spec.register(core_api_blueprint)
@@ -0,0 +1,7 @@
1
+ from platzky import FeatureFlag
2
+
3
+ CategoriesHelp = FeatureFlag(alias="CATEGORIES_HELP", description="Show category help text")
4
+ UseLazyLoading = FeatureFlag(
5
+ alias="USE_LAZY_LOADING", description="Enable lazy loading of location fields"
6
+ )
7
+ EnableAdminPanel = FeatureFlag(alias="ENABLE_ADMIN_PANEL", description="Enable admin panel")
goodmap/goodmap.py CHANGED
@@ -1,11 +1,13 @@
1
1
  """Goodmap engine with location management and admin interface."""
2
2
 
3
+ import logging
3
4
  import os
4
5
 
5
6
  from flask import Blueprint, redirect, render_template, session
6
7
  from flask_wtf.csrf import CSRFProtect, generate_csrf
7
8
  from platzky import platzky
8
- from platzky.config import languages_dict
9
+ from platzky.attachment import create_attachment_class
10
+ from platzky.config import AttachmentConfig, languages_dict
9
11
  from platzky.models import CmsModule
10
12
 
11
13
  from goodmap.admin_api import admin_pages
@@ -16,6 +18,9 @@ from goodmap.db import (
16
18
  extend_db_with_goodmap_queries,
17
19
  get_location_obligatory_fields,
18
20
  )
21
+ from goodmap.feature_flags import EnableAdminPanel, UseLazyLoading
22
+
23
+ logger = logging.getLogger(__name__)
19
24
 
20
25
 
21
26
  def create_app(config_path: str) -> platzky.Engine:
@@ -31,20 +36,6 @@ def create_app(config_path: str) -> platzky.Engine:
31
36
  return create_app_from_config(config)
32
37
 
33
38
 
34
- # TODO Checking if there is a feature flag secition should be part of configs logic not client app
35
- def is_feature_enabled(config: GoodmapConfig, feature: str) -> bool:
36
- """Check if a feature flag is enabled in the configuration.
37
-
38
- Args:
39
- config: Goodmap configuration object
40
- feature: Name of the feature flag to check
41
-
42
- Returns:
43
- bool: True if feature is enabled, False otherwise
44
- """
45
- return config.feature_flags.get(feature, False) if config.feature_flags else False
46
-
47
-
48
39
  def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
49
40
  """Create and configure Goodmap application from config object.
50
41
 
@@ -69,7 +60,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
69
60
  if "MAX_CONTENT_LENGTH" not in app.config:
70
61
  app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 # 100KB
71
62
 
72
- if is_feature_enabled(config, "USE_LAZY_LOADING"):
63
+ if app.is_enabled(UseLazyLoading):
73
64
  location_obligatory_fields = get_location_obligatory_fields(app.db)
74
65
  # Extend db with goodmap queries first so we can use the bound method
75
66
  location_model = create_location_model(location_obligatory_fields, {})
@@ -97,12 +88,24 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
97
88
 
98
89
  CSRFProtect(app)
99
90
 
91
+ # Create Attachment class for photo uploads
92
+ # JPEG-only: universal browser/device support, good compression for location photos,
93
+ # no transparency needed. PNG/WebP can be added if user demand warrants it.
94
+ photo_attachment_config = AttachmentConfig(
95
+ allowed_mime_types=frozenset({"image/jpeg"}),
96
+ allowed_extensions=frozenset({"jpg", "jpeg"}),
97
+ max_size=5 * 1024 * 1024, # 5MB - reasonable for location photos
98
+ )
99
+ PhotoAttachment = create_attachment_class(photo_attachment_config)
100
+
100
101
  cp = core_pages(
101
102
  app.db,
102
103
  languages_dict(config.languages),
103
104
  app.notify,
104
105
  generate_csrf,
105
106
  location_model,
107
+ photo_attachment_class=PhotoAttachment,
108
+ photo_attachment_config=photo_attachment_config,
106
109
  feature_flags=config.feature_flags,
107
110
  )
108
111
  app.register_blueprint(cp)
@@ -159,7 +162,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
159
162
  Returns:
160
163
  Rendered goodmap-admin.html template or redirect to login
161
164
  """
162
- if not is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
165
+ if not app.is_enabled(EnableAdminPanel):
163
166
  return redirect("/")
164
167
 
165
168
  user = session.get("user", None)
@@ -178,7 +181,7 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
178
181
 
179
182
  app.register_blueprint(goodmap)
180
183
 
181
- if is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
184
+ if app.is_enabled(EnableAdminPanel):
182
185
  admin_bp = admin_pages(app.db, location_model)
183
186
  app.register_blueprint(admin_bp)
184
187
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goodmap
3
- Version: 1.3.1
3
+ Version: 1.5.0
4
4
  Summary: Map engine to serve all the people :)
5
5
  Author: Krzysztof Kolodzinski
6
6
  Author-email: krzysztof.kolodzinski@problematy.pl
@@ -20,11 +20,11 @@ Requires-Dist: aiohttp (>=3.8.4,<4.0.0)
20
20
  Requires-Dist: deprecation (>=2.1.0,<3.0.0)
21
21
  Requires-Dist: google-cloud-storage (>=2.7.0,<3.0.0)
22
22
  Requires-Dist: gql (>=3.4.0,<4.0.0)
23
- Requires-Dist: gunicorn (>=20.1,<24.0)
23
+ Requires-Dist: gunicorn (>=20.1,<25.0)
24
24
  Requires-Dist: humanize (>=4.6.0,<5.0.0)
25
25
  Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
26
26
  Requires-Dist: numpy (>=2.2.0,<3.0.0)
27
- Requires-Dist: platzky (>=1.0.0,<2.0.0)
27
+ Requires-Dist: platzky (>=1.4.1,<2.0.0)
28
28
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
29
29
  Requires-Dist: pysupercluster-problematy (>=0.7.8,<0.8.0)
30
30
  Requires-Dist: scipy (>=1.15.1,<2.0.0)
@@ -1,20 +1,21 @@
1
1
  goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
- goodmap/admin_api.py,sha256=dt0R_ZVREDy06RtnE-fTYYezU4NaZe5CBl2KGJx87Jw,10146
2
+ goodmap/admin_api.py,sha256=5tvHeqknG8WmhBYmIHlQHTOUA-zaT8FKaAyyLdvX2EE,10290
3
3
  goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
4
4
  goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
5
5
  goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
6
6
  goodmap/core.py,sha256=AgdGLfeJvL7TlTX893NR2YdCS8EuXx93Gx6ndvWws7s,2673
7
- goodmap/core_api.py,sha256=Av-kim3NjkHQD4B139O3IyXGl7PABzkyVui1hmkiv5I,18089
7
+ goodmap/core_api.py,sha256=CnQHXzSxym-URHE4teVkyOAT-iIMWWEX0cP7R_01Tg4,19981
8
8
  goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
9
9
  goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
10
10
  goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
11
11
  goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
12
+ goodmap/feature_flags.py,sha256=-hiqTX4OlhfY_4M1Kvy-_z1Fx6YTaFi3SVGYa0Pamcw,334
12
13
  goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
13
- goodmap/goodmap.py,sha256=uWjnOt00gWwAFydDw6EMD-EgAmlp-6ZSEdhSevDUBKg,7273
14
+ goodmap/goodmap.py,sha256=f69aUloRe4bpx2JRwZFiHOeUSk0Exq-Qv2FCdwiwLA0,7541
14
15
  goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
15
16
  goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
16
17
  goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
17
- goodmap-1.3.1.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
18
- goodmap-1.3.1.dist-info/METADATA,sha256=bdXqFG7LTW-rAtkmXCKgu5pq9KINVOvRAUsaCuHHEHQ,5798
19
- goodmap-1.3.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
20
- goodmap-1.3.1.dist-info/RECORD,,
18
+ goodmap-1.5.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
19
+ goodmap-1.5.0.dist-info/METADATA,sha256=PpLVvzHAcIIKEUVAqBkTZ1zcLXNnFFUivPUFIieiu6k,5798
20
+ goodmap-1.5.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
21
+ goodmap-1.5.0.dist-info/RECORD,,