goodmap 1.1.14__py3-none-any.whl → 1.3.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.
@@ -1,29 +1,36 @@
1
- from typing import Any, Type, cast
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: tuple[float, float]
17
- uuid: str
23
+ """Base model for location data with position validation and error enrichment.
18
24
 
19
- @field_validator("position")
20
- @classmethod
21
- def position_must_be_valid(cls, v):
22
- if v[0] < -90 or v[0] > 90:
23
- raise ValueError("latitude must be in range -90 to 90")
24
- if v[1] < -180 or v[1] > 180:
25
- raise ValueError("longitude must be in range -180 to 180")
26
- return v
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 basic_info(self):
47
- return {
48
- "uuid": self.uuid,
49
- "position": self.position,
50
- "remark": bool(getattr(self, "remark", False)),
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
- def create_location_model(obligatory_fields: list[tuple[str, Type[Any]]]) -> Type[BaseModel]:
55
- fields = {
56
- field_name: (field_type, Field(...)) for (field_name, field_type) in obligatory_fields
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
- **cast(dict[str, Any], fields),
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
@@ -6,37 +8,92 @@ from platzky import platzky
6
8
  from platzky.config import languages_dict
7
9
  from platzky.models import CmsModule
8
10
 
11
+ from goodmap.admin_api import admin_pages
9
12
  from goodmap.config import GoodmapConfig
10
13
  from goodmap.core_api import core_pages
11
14
  from goodmap.data_models.location import create_location_model
12
- from goodmap.db import extend_db_with_goodmap_queries, get_location_obligatory_fields
15
+ from goodmap.db import (
16
+ extend_db_with_goodmap_queries,
17
+ get_location_obligatory_fields,
18
+ )
13
19
 
14
20
 
15
21
  def create_app(config_path: str) -> platzky.Engine:
22
+ """Create Goodmap application from YAML configuration file.
23
+
24
+ Args:
25
+ config_path: Path to YAML configuration file
26
+
27
+ Returns:
28
+ platzky.Engine: Configured Flask application
29
+ """
16
30
  config = GoodmapConfig.parse_yaml(config_path)
17
31
  return create_app_from_config(config)
18
32
 
19
33
 
20
34
  # TODO Checking if there is a feature flag secition should be part of configs logic not client app
21
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
+ """
22
45
  return config.feature_flags.get(feature, False) if config.feature_flags else False
23
46
 
24
47
 
25
48
  def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
49
+ """Create and configure Goodmap application from config object.
50
+
51
+ Sets up location models, database queries, CSRF protection, API blueprints,
52
+ and admin interface based on the provided configuration.
53
+
54
+ Args:
55
+ config: Goodmap configuration object
56
+
57
+ Returns:
58
+ platzky.Engine: Fully configured Flask application with Goodmap features
59
+ """
26
60
  directory = os.path.dirname(os.path.realpath(__file__))
27
61
 
28
62
  locale_dir = os.path.join(directory, "locale")
29
63
  config.translation_directories.append(locale_dir)
30
64
  app = platzky.create_app_from_config(config)
31
65
 
66
+ # SECURITY: Set maximum request body size to 100KB (prevents memory exhaustion)
67
+ # This protects against large file uploads and JSON payloads
68
+ # Based on calculation: ~6.5KB max legitimate payload + multipart overhead
69
+ if "MAX_CONTENT_LENGTH" not in app.config:
70
+ app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 # 100KB
71
+
32
72
  if is_feature_enabled(config, "USE_LAZY_LOADING"):
33
73
  location_obligatory_fields = get_location_obligatory_fields(app.db)
74
+ # Extend db with goodmap queries first so we can use the bound method
75
+ location_model = create_location_model(location_obligatory_fields, {})
76
+ app.db = extend_db_with_goodmap_queries(app.db, location_model)
77
+
78
+ # Use the extended db method directly (already bound by extend_db_with_goodmap_queries)
79
+ try:
80
+ category_data = app.db.get_category_data() # type: ignore[attr-defined]
81
+ categories = category_data.get("categories", {})
82
+ except (KeyError, AttributeError):
83
+ # Handle case where categories don't exist in the data
84
+ categories = {}
85
+
86
+ # Recreate location model with categories if we got them
87
+ if categories:
88
+ location_model = create_location_model(location_obligatory_fields, categories)
89
+ app.db = extend_db_with_goodmap_queries(app.db, location_model)
34
90
  else:
35
91
  location_obligatory_fields = []
92
+ categories = {}
93
+ location_model = create_location_model(location_obligatory_fields, categories)
94
+ app.db = extend_db_with_goodmap_queries(app.db, location_model)
36
95
 
37
- location_model = create_location_model(location_obligatory_fields)
38
-
39
- app.db = extend_db_with_goodmap_queries(app.db, location_model)
96
+ app.extensions["goodmap"] = {"location_obligatory_fields": location_obligatory_fields}
40
97
 
41
98
  CSRFProtect(app)
42
99
 
@@ -49,18 +106,39 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
49
106
  feature_flags=config.feature_flags,
50
107
  )
51
108
  app.register_blueprint(cp)
109
+
52
110
  goodmap = Blueprint("goodmap", __name__, url_prefix="/", template_folder="templates")
53
111
 
54
112
  @goodmap.route("/")
55
113
  def index():
114
+ """Render main map interface with location schema.
115
+
116
+ Prepares and passes location schema including obligatory fields and
117
+ categories to the frontend for dynamic form generation.
118
+
119
+ Returns:
120
+ Rendered map.html template with feature flags and location schema
121
+ """
56
122
  # Prepare location schema for frontend dynamic forms
57
- # Convert categories dict_keys to a proper dict for JSON serialization
58
- category_data = app.db.get_category_data()
123
+ # Include full schema from Pydantic model for better type information
124
+ category_data = app.db.get_category_data() # type: ignore[attr-defined]
59
125
  categories = category_data.get("categories", {})
60
126
 
61
- location_schema = {
62
- "obligatory_fields": location_obligatory_fields,
63
- "categories": categories,
127
+ # Get full JSON schema from Pydantic model
128
+ model_json_schema = location_model.model_json_schema()
129
+ properties = model_json_schema.get("properties", {})
130
+
131
+ # Filter out uuid and position from properties for frontend form
132
+ form_fields = {
133
+ name: spec for name, spec in properties.items() if name not in ("uuid", "position")
134
+ }
135
+
136
+ location_schema = { # TODO remove backward compatibility - deprecation
137
+ "obligatory_fields": app.extensions["goodmap"][
138
+ "location_obligatory_fields"
139
+ ], # Backward compatibility
140
+ "categories": categories, # Backward compatibility
141
+ "fields": form_fields,
64
142
  }
65
143
 
66
144
  return render_template(
@@ -72,6 +150,18 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
72
150
 
73
151
  @goodmap.route("/goodmap-admin")
74
152
  def admin():
153
+ """Render admin interface for managing map data.
154
+
155
+ Requires user to be logged in (redirects to /admin if not).
156
+ Provides admin panel for managing locations, suggestions, and reports.
157
+ Only available when ENABLE_ADMIN_PANEL feature flag is enabled.
158
+
159
+ Returns:
160
+ Rendered goodmap-admin.html template or redirect to login
161
+ """
162
+ if not is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
163
+ return redirect("/")
164
+
75
165
  user = session.get("user", None)
76
166
  if not user:
77
167
  return redirect("/admin")
@@ -87,14 +177,19 @@ def create_app_from_config(config: GoodmapConfig) -> platzky.Engine:
87
177
  )
88
178
 
89
179
  app.register_blueprint(goodmap)
90
- goodmap_cms_modules = CmsModule.model_validate(
91
- {
92
- "name": "Map admin panel",
93
- "description": "Admin panel for managing map data",
94
- "slug": "goodmap-admin",
95
- "template": "goodmap-admin.html",
96
- }
97
- )
98
- app.add_cms_module(goodmap_cms_modules)
180
+
181
+ if is_feature_enabled(config, "ENABLE_ADMIN_PANEL"):
182
+ admin_bp = admin_pages(app.db, location_model)
183
+ app.register_blueprint(admin_bp)
184
+
185
+ goodmap_cms_modules = CmsModule.model_validate(
186
+ {
187
+ "name": "Map admin panel",
188
+ "description": "Admin panel for managing map data",
189
+ "slug": "goodmap-admin",
190
+ "template": "goodmap-admin.html",
191
+ }
192
+ )
193
+ app.add_cms_module(goodmap_cms_modules)
99
194
 
100
195
  return app
@@ -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.1.14
3
+ Version: 1.3.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,20 @@
1
+ goodmap/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ goodmap/admin_api.py,sha256=dt0R_ZVREDy06RtnE-fTYYezU4NaZe5CBl2KGJx87Jw,10146
3
+ goodmap/api_models.py,sha256=Bv4OTGuckNneCrxaQ1Y_PMeu7YFLvGUqU2EorvDlUjY,3438
4
+ goodmap/clustering.py,sha256=ULB-fPNOUDblgpBK4vzuo0o2yqIcvG84F3R6Za2X_l4,2905
5
+ goodmap/config.py,sha256=CsmC1zuvVab90VW50dtARHbFJpy2vfsIfbque8Zgc-U,1313
6
+ goodmap/core.py,sha256=AgdGLfeJvL7TlTX893NR2YdCS8EuXx93Gx6ndvWws7s,2673
7
+ goodmap/core_api.py,sha256=3NBzuiMTGWCNZfrDIeCvc6RaBu4fWxITT7hRjYQF-yc,17082
8
+ goodmap/data_models/location.py,sha256=_I27R06ovEL9ctv_SZ3yoLL-RwmyE3VDsVOG4a89q50,6798
9
+ goodmap/data_validator.py,sha256=lBmVAPxvSmEOdUGeVYSjUvVVmKfPyq4CWoHfczTtEMM,4090
10
+ goodmap/db.py,sha256=TcqYGbK5yk6S735Si1AzjNqcbB1nsd9pFGOy5qN9Vec,46589
11
+ goodmap/exceptions.py,sha256=jkFAUoc5LHk8iPjxHxbcRp8W6qFCSEA25A8XaSwxwyo,2906
12
+ goodmap/formatter.py,sha256=4rqcg9A9Y9opAi7eb8kMDdUC03M3uzZgCxx29cvvIag,1403
13
+ goodmap/goodmap.py,sha256=uWjnOt00gWwAFydDw6EMD-EgAmlp-6ZSEdhSevDUBKg,7273
14
+ goodmap/json_security.py,sha256=EHAxNlb16AVwphgf4F7yObtMZpbR9M538dwn_STRcMo,3275
15
+ goodmap/templates/goodmap-admin.html,sha256=LSiOZ9-n29CnlfVNwdgmXwT7Xe7t5gvGh1xSrFGqOIY,35669
16
+ goodmap/templates/map.html,sha256=Uk7FFrZwvHZvG0DDaQrGW5ZrIMD21XrJzMub76uIlAg,4348
17
+ goodmap-1.3.0.dist-info/LICENSE.md,sha256=nkCQOR7uheLRvHRfXmwx9LhBnMcPeBU9d4ebLojDiQU,1067
18
+ goodmap-1.3.0.dist-info/METADATA,sha256=hFruvLR8o7jb9LIQtWYGRR90h7Iyffiq2DSS8X0JdfI,5798
19
+ goodmap-1.3.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
20
+ goodmap-1.3.0.dist-info/RECORD,,
@@ -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,,