goodmap 1.1.14__py3-none-any.whl → 1.2.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/api_models.py +105 -0
- goodmap/core.py +39 -0
- goodmap/core_api.py +518 -407
- goodmap/data_models/location.py +154 -23
- goodmap/formatter.py +20 -0
- goodmap/goodmap.py +92 -8
- goodmap/json_security.py +102 -0
- {goodmap-1.1.14.dist-info → goodmap-1.2.0.dist-info}/METADATA +2 -2
- goodmap-1.2.0.dist-info/RECORD +19 -0
- goodmap-1.1.14.dist-info/RECORD +0 -17
- {goodmap-1.1.14.dist-info → goodmap-1.2.0.dist-info}/LICENSE.md +0 -0
- {goodmap-1.1.14.dist-info → goodmap-1.2.0.dist-info}/WHEEL +0 -0
goodmap/data_models/location.py
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
"""Pydantic models for location data validation and schema generation."""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
4
|
+
from typing import Annotated, Any, Type, cast
|
|
5
|
+
|
|
6
|
+
from annotated_types import Ge, Le
|
|
3
7
|
from pydantic import (
|
|
8
|
+
AfterValidator,
|
|
4
9
|
BaseModel,
|
|
5
10
|
Field,
|
|
6
11
|
ValidationError,
|
|
7
12
|
create_model,
|
|
8
|
-
field_validator,
|
|
9
13
|
model_validator,
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
from goodmap.exceptions import LocationValidationError
|
|
13
17
|
|
|
18
|
+
Latitude = Annotated[float, Ge(-90), Le(90)]
|
|
19
|
+
Longitude = Annotated[float, Ge(-180), Le(180)]
|
|
20
|
+
|
|
14
21
|
|
|
15
22
|
class LocationBase(BaseModel, extra="allow"):
|
|
16
|
-
position
|
|
17
|
-
uuid: str
|
|
23
|
+
"""Base model for location data with position validation and error enrichment.
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Attributes:
|
|
26
|
+
position: Tuple of (latitude, longitude) coordinates
|
|
27
|
+
uuid: Unique identifier for the location
|
|
28
|
+
remark: Optional remark text for the location
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
position: tuple[Latitude, Longitude]
|
|
32
|
+
uuid: str = Field(..., max_length=100) # TODO make this UUID and deprecate string
|
|
33
|
+
remark: str | None = None
|
|
27
34
|
|
|
28
35
|
@model_validator(mode="before")
|
|
29
36
|
@classmethod
|
|
@@ -43,22 +50,146 @@ class LocationBase(BaseModel, extra="allow"):
|
|
|
43
50
|
uuid = data.get("uuid") if isinstance(data, dict) else None
|
|
44
51
|
raise LocationValidationError(e, uuid=uuid) from e
|
|
45
52
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
def model_dump(self, **kwargs) -> dict[str, Any]:
|
|
54
|
+
"""Serialize model, excluding None values by default for backward compatibility."""
|
|
55
|
+
kwargs.setdefault("exclude_none", True)
|
|
56
|
+
return super().model_dump(**kwargs)
|
|
57
|
+
|
|
58
|
+
def basic_info(self) -> dict[str, Any]:
|
|
59
|
+
"""Get basic location information summary."""
|
|
60
|
+
data = self.model_dump(include={"uuid", "position"})
|
|
61
|
+
data["remark"] = bool(self.remark)
|
|
62
|
+
return data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_TYPE_MAPPING: dict[str, type] = {
|
|
66
|
+
"str": str,
|
|
67
|
+
"list": list,
|
|
68
|
+
"int": int,
|
|
69
|
+
"float": float,
|
|
70
|
+
"bool": bool,
|
|
71
|
+
"dict": dict,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_MAX_LIST_ITEM_LENGTH = 100
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _make_list_validator(allowed: list[str] | None):
|
|
78
|
+
"""Create a validator for list items with optional enum constraint."""
|
|
79
|
+
|
|
80
|
+
def validate(v: list[Any]) -> list[Any]:
|
|
81
|
+
for item in v:
|
|
82
|
+
if allowed is not None and item not in allowed:
|
|
83
|
+
raise ValueError(f"must be one of {allowed}, got '{item}'")
|
|
84
|
+
if isinstance(item, str) and len(item) > _MAX_LIST_ITEM_LENGTH:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"list item too long (max {_MAX_LIST_ITEM_LENGTH} chars), got {len(item)}"
|
|
87
|
+
)
|
|
88
|
+
return v
|
|
89
|
+
|
|
90
|
+
return validate
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _make_str_validator(allowed: list[str]):
|
|
94
|
+
"""Create a validator that checks string value is in allowed list."""
|
|
95
|
+
|
|
96
|
+
def validate(v: str) -> str:
|
|
97
|
+
if v not in allowed:
|
|
98
|
+
raise ValueError(f"must be one of {allowed}, got '{v}'")
|
|
99
|
+
return v
|
|
100
|
+
|
|
101
|
+
return validate
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _normalize_field_type(field_type_input: str | Type[Any]) -> str:
|
|
105
|
+
"""Convert field type input to string, emitting deprecation warning for type objects."""
|
|
106
|
+
if isinstance(field_type_input, type):
|
|
107
|
+
warnings.warn(
|
|
108
|
+
f"Passing Python type objects to create_location_model is deprecated. "
|
|
109
|
+
f"Use string type names instead: '{field_type_input.__name__}' "
|
|
110
|
+
f"instead of {field_type_input}. "
|
|
111
|
+
f"Support for type objects will be removed in version 2.0.0.",
|
|
112
|
+
DeprecationWarning,
|
|
113
|
+
stacklevel=3,
|
|
114
|
+
)
|
|
115
|
+
return field_type_input.__name__
|
|
116
|
+
return field_type_input
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_field_definition(
|
|
120
|
+
field_type_str: str, allowed_values: list[str] | None
|
|
121
|
+
) -> tuple[Any, Any]:
|
|
122
|
+
"""Build a complete field definition based on type and constraints."""
|
|
123
|
+
is_list = field_type_str.startswith("list")
|
|
124
|
+
|
|
125
|
+
if is_list:
|
|
126
|
+
field_type = Annotated[list[Any], AfterValidator(_make_list_validator(allowed_values))]
|
|
127
|
+
if allowed_values:
|
|
128
|
+
return (
|
|
129
|
+
field_type,
|
|
130
|
+
Field(
|
|
131
|
+
...,
|
|
132
|
+
description=f"Allowed values: {', '.join(allowed_values)}",
|
|
133
|
+
max_length=20,
|
|
134
|
+
json_schema_extra=cast(Any, {"enum_items": allowed_values}),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
return (field_type, Field(..., max_length=20))
|
|
138
|
+
|
|
139
|
+
if allowed_values:
|
|
140
|
+
field_type = Annotated[str, AfterValidator(_make_str_validator(allowed_values))]
|
|
141
|
+
return (
|
|
142
|
+
field_type,
|
|
143
|
+
Field(
|
|
144
|
+
...,
|
|
145
|
+
description=f"Allowed values: {', '.join(allowed_values)}",
|
|
146
|
+
max_length=200,
|
|
147
|
+
json_schema_extra=cast(Any, {"enum": allowed_values}),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
base_type = _TYPE_MAPPING.get(field_type_str, str)
|
|
152
|
+
if base_type is str:
|
|
153
|
+
return (base_type, Field(..., max_length=200))
|
|
154
|
+
return (base_type, Field(...))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def create_location_model(
|
|
158
|
+
obligatory_fields: list[tuple[str, str]] | list[tuple[str, Type[Any]]],
|
|
159
|
+
categories: dict[str, list[str]] | None = None,
|
|
160
|
+
) -> Type[BaseModel]:
|
|
161
|
+
"""Dynamically create a Location model with additional required fields.
|
|
162
|
+
|
|
163
|
+
Supports both string type names (recommended) and Python type objects (deprecated).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
obligatory_fields: List of (field_name, field_type) tuples for required fields.
|
|
167
|
+
field_type can be either:
|
|
168
|
+
- String type name: "str", "list", "int", "float", "bool", "dict"
|
|
169
|
+
- Python type object: str, list, int, etc. (deprecated)
|
|
170
|
+
categories: Optional dict mapping field names to allowed values (enums)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
A Location model class extending LocationBase with additional fields
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> # Recommended: String type names
|
|
177
|
+
>>> model = create_location_model([("name", "str"), ("tags", "list")])
|
|
52
178
|
|
|
179
|
+
>>> # Deprecated: Python type objects (supported for backward compatibility)
|
|
180
|
+
>>> model = create_location_model([("name", str), ("tags", list)])
|
|
181
|
+
"""
|
|
182
|
+
categories = categories or {}
|
|
183
|
+
fields: dict[str, Any] = {}
|
|
53
184
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
185
|
+
for field_name, field_type_input in obligatory_fields:
|
|
186
|
+
field_type_str = _normalize_field_type(field_type_input)
|
|
187
|
+
allowed_values = categories.get(field_name)
|
|
188
|
+
fields[field_name] = _build_field_definition(field_type_str, allowed_values)
|
|
58
189
|
|
|
59
190
|
return create_model(
|
|
60
191
|
"Location",
|
|
61
192
|
__base__=LocationBase,
|
|
62
193
|
__module__="goodmap.data_models.location",
|
|
63
|
-
**
|
|
194
|
+
**fields,
|
|
64
195
|
)
|
goodmap/formatter.py
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
"""Formatters for translating and preparing location data for display."""
|
|
2
|
+
|
|
1
3
|
from flask_babel import gettext, lazy_gettext
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
def safe_gettext(text):
|
|
7
|
+
"""Safely apply gettext translation to various data types.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
text: Text to translate (str, list, or dict)
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Translated text in same format as input
|
|
14
|
+
"""
|
|
5
15
|
if isinstance(text, list):
|
|
6
16
|
return list(map(gettext, text))
|
|
7
17
|
elif isinstance(text, dict):
|
|
@@ -11,6 +21,16 @@ def safe_gettext(text):
|
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
def prepare_pin(place, visible_fields, meta_data):
|
|
24
|
+
"""Prepare location data for map pin display with translations.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
place: Location data dictionary
|
|
28
|
+
visible_fields: List of field names to display in pin
|
|
29
|
+
meta_data: List of metadata field names
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
dict: Formatted pin data with title, subtitle, position, metadata, and translated fields
|
|
33
|
+
"""
|
|
14
34
|
pin_data = {
|
|
15
35
|
"title": place["name"],
|
|
16
36
|
"subtitle": lazy_gettext(place["type_of_place"]), # TODO this should not be obligatory
|
goodmap/goodmap.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Goodmap engine with location management and admin interface."""
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
|
|
3
5
|
from flask import Blueprint, redirect, render_template, session
|
|
@@ -9,34 +11,88 @@ from platzky.models import CmsModule
|
|
|
9
11
|
from goodmap.config import GoodmapConfig
|
|
10
12
|
from goodmap.core_api import core_pages
|
|
11
13
|
from goodmap.data_models.location import create_location_model
|
|
12
|
-
from goodmap.db import
|
|
14
|
+
from goodmap.db import (
|
|
15
|
+
extend_db_with_goodmap_queries,
|
|
16
|
+
get_location_obligatory_fields,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
def create_app(config_path: str) -> platzky.Engine:
|
|
21
|
+
"""Create Goodmap application from YAML configuration file.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
config_path: Path to YAML configuration file
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
platzky.Engine: Configured Flask application
|
|
28
|
+
"""
|
|
16
29
|
config = GoodmapConfig.parse_yaml(config_path)
|
|
17
30
|
return create_app_from_config(config)
|
|
18
31
|
|
|
19
32
|
|
|
20
33
|
# TODO Checking if there is a feature flag secition should be part of configs logic not client app
|
|
21
34
|
def is_feature_enabled(config: GoodmapConfig, feature: str) -> bool:
|
|
35
|
+
"""Check if a feature flag is enabled in the configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Goodmap configuration object
|
|
39
|
+
feature: Name of the feature flag to check
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool: True if feature is enabled, False otherwise
|
|
43
|
+
"""
|
|
22
44
|
return config.feature_flags.get(feature, False) if config.feature_flags else False
|
|
23
45
|
|
|
24
46
|
|
|
25
47
|
def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
48
|
+
"""Create and configure Goodmap application from config object.
|
|
49
|
+
|
|
50
|
+
Sets up location models, database queries, CSRF protection, API blueprints,
|
|
51
|
+
and admin interface based on the provided configuration.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config: Goodmap configuration object
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
platzky.Engine: Fully configured Flask application with Goodmap features
|
|
58
|
+
"""
|
|
26
59
|
directory = os.path.dirname(os.path.realpath(__file__))
|
|
27
60
|
|
|
28
61
|
locale_dir = os.path.join(directory, "locale")
|
|
29
62
|
config.translation_directories.append(locale_dir)
|
|
30
63
|
app = platzky.create_app_from_config(config)
|
|
31
64
|
|
|
65
|
+
# SECURITY: Set maximum request body size to 100KB (prevents memory exhaustion)
|
|
66
|
+
# This protects against large file uploads and JSON payloads
|
|
67
|
+
# Based on calculation: ~6.5KB max legitimate payload + multipart overhead
|
|
68
|
+
if "MAX_CONTENT_LENGTH" not in app.config:
|
|
69
|
+
app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 # 100KB
|
|
70
|
+
|
|
32
71
|
if is_feature_enabled(config, "USE_LAZY_LOADING"):
|
|
33
72
|
location_obligatory_fields = get_location_obligatory_fields(app.db)
|
|
73
|
+
# Extend db with goodmap queries first so we can use the bound method
|
|
74
|
+
location_model = create_location_model(location_obligatory_fields, {})
|
|
75
|
+
app.db = extend_db_with_goodmap_queries(app.db, location_model)
|
|
76
|
+
|
|
77
|
+
# Use the extended db method directly (already bound by extend_db_with_goodmap_queries)
|
|
78
|
+
try:
|
|
79
|
+
category_data = app.db.get_category_data()
|
|
80
|
+
categories = category_data.get("categories", {})
|
|
81
|
+
except (KeyError, AttributeError):
|
|
82
|
+
# Handle case where categories don't exist in the data
|
|
83
|
+
categories = {}
|
|
84
|
+
|
|
85
|
+
# Recreate location model with categories if we got them
|
|
86
|
+
if categories:
|
|
87
|
+
location_model = create_location_model(location_obligatory_fields, categories)
|
|
88
|
+
app.db = extend_db_with_goodmap_queries(app.db, location_model)
|
|
34
89
|
else:
|
|
35
90
|
location_obligatory_fields = []
|
|
91
|
+
categories = {}
|
|
92
|
+
location_model = create_location_model(location_obligatory_fields, categories)
|
|
93
|
+
app.db = extend_db_with_goodmap_queries(app.db, location_model)
|
|
36
94
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
app.db = extend_db_with_goodmap_queries(app.db, location_model)
|
|
95
|
+
app.extensions["goodmap"] = {"location_obligatory_fields": location_obligatory_fields}
|
|
40
96
|
|
|
41
97
|
CSRFProtect(app)
|
|
42
98
|
|
|
@@ -53,14 +109,34 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
53
109
|
|
|
54
110
|
@goodmap.route("/")
|
|
55
111
|
def index():
|
|
112
|
+
"""Render main map interface with location schema.
|
|
113
|
+
|
|
114
|
+
Prepares and passes location schema including obligatory fields and
|
|
115
|
+
categories to the frontend for dynamic form generation.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Rendered map.html template with feature flags and location schema
|
|
119
|
+
"""
|
|
56
120
|
# Prepare location schema for frontend dynamic forms
|
|
57
|
-
#
|
|
121
|
+
# Include full schema from Pydantic model for better type information
|
|
58
122
|
category_data = app.db.get_category_data()
|
|
59
123
|
categories = category_data.get("categories", {})
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
# Get full JSON schema from Pydantic model
|
|
126
|
+
model_json_schema = location_model.model_json_schema()
|
|
127
|
+
properties = model_json_schema.get("properties", {})
|
|
128
|
+
|
|
129
|
+
# Filter out uuid and position from properties for frontend form
|
|
130
|
+
form_fields = {
|
|
131
|
+
name: spec for name, spec in properties.items() if name not in ("uuid", "position")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
location_schema = { # TODO remove backward compatibility - deprecation
|
|
135
|
+
"obligatory_fields": app.extensions["goodmap"][
|
|
136
|
+
"location_obligatory_fields"
|
|
137
|
+
], # Backward compatibility
|
|
138
|
+
"categories": categories, # Backward compatibility
|
|
139
|
+
"fields": form_fields,
|
|
64
140
|
}
|
|
65
141
|
|
|
66
142
|
return render_template(
|
|
@@ -72,6 +148,14 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
|
|
|
72
148
|
|
|
73
149
|
@goodmap.route("/goodmap-admin")
|
|
74
150
|
def admin():
|
|
151
|
+
"""Render admin interface for managing map data.
|
|
152
|
+
|
|
153
|
+
Requires user to be logged in (redirects to /admin if not).
|
|
154
|
+
Provides admin panel for managing locations, suggestions, and reports.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Rendered goodmap-admin.html template or redirect to login
|
|
158
|
+
"""
|
|
75
159
|
user = session.get("user", None)
|
|
76
160
|
if not user:
|
|
77
161
|
return redirect("/admin")
|
goodmap/json_security.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Secure JSON parsing utilities to prevent DoS attacks."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
# Security constants
|
|
7
|
+
MAX_JSON_DEPTH = 10 # Default max depth for general JSON parsing
|
|
8
|
+
# Strict limit for location data: primitives and arrays/objects of primitives (depth 0-2)
|
|
9
|
+
MAX_JSON_DEPTH_LOCATION = 2
|
|
10
|
+
MAX_JSON_SIZE = 50 * 1024 # 50KB in bytes (reasonable for individual form fields)
|
|
11
|
+
MAX_STRING_LENGTH = 1_000 # 1000 chars per string
|
|
12
|
+
MAX_ARRAY_ITEMS = 100 # 100 items per array
|
|
13
|
+
MAX_OBJECT_KEYS = 50 # 50 keys per object
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JSONDepthError(ValueError):
|
|
17
|
+
"""Raised when JSON nesting exceeds maximum depth."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JSONSizeError(ValueError):
|
|
23
|
+
"""Raised when JSON size exceeds maximum allowed size."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def safe_json_loads(
|
|
29
|
+
json_string: str,
|
|
30
|
+
max_depth: int = MAX_JSON_DEPTH,
|
|
31
|
+
max_size: int = MAX_JSON_SIZE,
|
|
32
|
+
) -> Any:
|
|
33
|
+
"""Parse JSON with depth and size limits to prevent DoS attacks.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
json_string: JSON string to parse
|
|
37
|
+
max_depth: Maximum nesting depth allowed (default: 10)
|
|
38
|
+
max_size: Maximum string size in bytes (default: 50KB)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Parsed JSON object
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
JSONSizeError: If input exceeds max_size
|
|
45
|
+
JSONDepthError: If nesting exceeds max_depth
|
|
46
|
+
ValueError: If JSON is invalid
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> safe_json_loads('{"key": "value"}')
|
|
50
|
+
{'key': 'value'}
|
|
51
|
+
>>> safe_json_loads('{"a":' * 20 + '1' + '}' * 20) # Too deep
|
|
52
|
+
Traceback: JSONDepthError
|
|
53
|
+
"""
|
|
54
|
+
# Size check before parsing
|
|
55
|
+
byte_size = len(json_string.encode("utf-8"))
|
|
56
|
+
if byte_size > max_size:
|
|
57
|
+
raise JSONSizeError(
|
|
58
|
+
f"JSON payload size ({byte_size} bytes) exceeds maximum "
|
|
59
|
+
f"allowed size ({max_size} bytes)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Parse JSON
|
|
63
|
+
try:
|
|
64
|
+
parsed = json.loads(json_string)
|
|
65
|
+
except json.JSONDecodeError as e:
|
|
66
|
+
# Re-raise with original error message
|
|
67
|
+
raise ValueError(f"Invalid JSON: {e}") from e
|
|
68
|
+
|
|
69
|
+
# Depth check after parsing
|
|
70
|
+
_check_depth(parsed, max_depth, current_depth=0)
|
|
71
|
+
|
|
72
|
+
return parsed
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _check_depth(obj: Any, max_depth: int, current_depth: int) -> None:
|
|
76
|
+
"""Recursively check JSON depth.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
obj: Object to check (dict, list, or primitive)
|
|
80
|
+
max_depth: Maximum allowed depth
|
|
81
|
+
current_depth: Current recursion depth
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
JSONDepthError: If depth exceeds max_depth
|
|
85
|
+
"""
|
|
86
|
+
if current_depth > max_depth:
|
|
87
|
+
raise JSONDepthError(
|
|
88
|
+
f"JSON nesting depth ({current_depth}) exceeds maximum allowed depth ({max_depth})"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if isinstance(obj, dict):
|
|
92
|
+
if len(obj) > MAX_OBJECT_KEYS:
|
|
93
|
+
raise JSONDepthError(f"Object has {len(obj)} keys, exceeding maximum {MAX_OBJECT_KEYS}")
|
|
94
|
+
for value in obj.values():
|
|
95
|
+
_check_depth(value, max_depth, current_depth + 1)
|
|
96
|
+
elif isinstance(obj, list):
|
|
97
|
+
if len(obj) > MAX_ARRAY_ITEMS:
|
|
98
|
+
raise JSONDepthError(f"Array has {len(obj)} items, exceeding maximum {MAX_ARRAY_ITEMS}")
|
|
99
|
+
for item in obj:
|
|
100
|
+
_check_depth(item, max_depth, current_depth + 1)
|
|
101
|
+
elif isinstance(obj, str) and len(obj) > MAX_STRING_LENGTH:
|
|
102
|
+
raise JSONDepthError(f"String length {len(obj)} exceeds maximum {MAX_STRING_LENGTH}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: goodmap
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Map engine to serve all the people :)
|
|
5
5
|
Author: Krzysztof Kolodzinski
|
|
6
6
|
Author-email: krzysztof.kolodzinski@problematy.pl
|
|
@@ -18,7 +18,6 @@ Requires-Dist: Flask-WTF (>=1.2.1,<2.0.0)
|
|
|
18
18
|
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
19
19
|
Requires-Dist: aiohttp (>=3.8.4,<4.0.0)
|
|
20
20
|
Requires-Dist: deprecation (>=2.1.0,<3.0.0)
|
|
21
|
-
Requires-Dist: flask-restx (>=1.3.0,<2.0.0)
|
|
22
21
|
Requires-Dist: google-cloud-storage (>=2.7.0,<3.0.0)
|
|
23
22
|
Requires-Dist: gql (>=3.4.0,<4.0.0)
|
|
24
23
|
Requires-Dist: gunicorn (>=20.1,<24.0)
|
|
@@ -29,6 +28,7 @@ Requires-Dist: platzky (>=1.0.0,<2.0.0)
|
|
|
29
28
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
30
29
|
Requires-Dist: pysupercluster-problematy (>=0.7.8,<0.8.0)
|
|
31
30
|
Requires-Dist: scipy (>=1.15.1,<2.0.0)
|
|
31
|
+
Requires-Dist: spectree (>=2.0.1,<3.0.0)
|
|
32
32
|
Requires-Dist: sphinx (>=8.0.0,<9.0.0) ; extra == "docs"
|
|
33
33
|
Requires-Dist: sphinx-rtd-theme (>=3.0.0,<4.0.0) ; extra == "docs"
|
|
34
34
|
Requires-Dist: tomli (>=2.0.0,<3.0.0) ; extra == "docs"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
+
goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
|
|
3
|
+
goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
|
|
4
|
+
goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
|
|
5
|
+
goodmap/core.py,sha256=AgdGLfeJvL7TlTX893NR2YdCS8EuXx93Gx6ndvWws7s,2673
|
|
6
|
+
goodmap/core_api.py,sha256=KuCj--d058t46uGrpapyuIQfVrzHp9TR2qLFzSR78SU,25097
|
|
7
|
+
goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
|
|
8
|
+
goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
|
|
9
|
+
goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
|
|
10
|
+
goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
|
|
11
|
+
goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
|
|
12
|
+
goodmap/goodmap.py,sha256=0DrebQhbdYSxf7Aac4tZ-z0pkZs-FClTT89NNHc0W0Y,6808
|
|
13
|
+
goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
|
|
14
|
+
goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
|
|
15
|
+
goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
|
|
16
|
+
goodmap-1.2.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
|
|
17
|
+
goodmap-1.2.0.dist-info/METADATA,sha256=qpiCIiQvKk6OxOQ8zbNwKjvBBiQtti85ALIn5pdP7hI,5798
|
|
18
|
+
goodmap-1.2.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
19
|
+
goodmap-1.2.0.dist-info/RECORD,,
|
goodmap-1.1.14.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
2
|
-
goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
|
|
3
|
-
goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
|
|
4
|
-
goodmap/core.py,sha256=rzMhOIYnR1jxTX6uHQJKIPLYxdUm4_v2d6LrtHtJpHU,1465
|
|
5
|
-
goodmap/core_api.py,sha256=Xzr9x89-K0gURcFvAypjKaboExdTKq1KfLfnGTvNG-Q,21144
|
|
6
|
-
goodmap/data_models/location.py,sha256=E2vUD9Sfr02eLbe-W0mBNtvGnqs7WqBP3XVyB-IgKq4,1951
|
|
7
|
-
goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
|
|
8
|
-
goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
|
|
9
|
-
goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
|
|
10
|
-
goodmap/formatter.py,sha256=VlUHcK1HtM_IEU0VE3S5TOkZLVheMdakvUeW2tCKdq0,783
|
|
11
|
-
goodmap/goodmap.py,sha256=LbmzYn4FHaP-Y5ZtQhMncGO2h18k2WYAsP5Hzw4oGUw,3392
|
|
12
|
-
goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
|
|
13
|
-
goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
|
|
14
|
-
goodmap-1.1.14.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
|
|
15
|
-
goodmap-1.1.14.dist-info/METADATA,sha256=00trO1OenoKs-zPIHz4aH4aciBZVqGy35XgP2DNF6Og,5802
|
|
16
|
-
goodmap-1.1.14.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
|
17
|
-
goodmap-1.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|