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.
@@ -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
@@ -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 extend_db_with_goodmap_queries, get_location_obligatory_fields
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
- location_model = create_location_model(location_obligatory_fields)
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
- # Convert categories dict_keys to a proper dict for JSON serialization
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
- location_schema = {
62
- "obligatory_fields": location_obligatory_fields,
63
- "categories": categories,
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")
@@ -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.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,,
@@ -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,,