Arnab Dey commited on
Commit
282197c
·
1 Parent(s): 6577b7a

Refactor code structure for improved readability and maintainability

Browse files
Files changed (5) hide show
  1. app.py +147 -20
  2. create_map_poster.py +181 -87
  3. pyproject.toml +1 -0
  4. requirements.txt +1 -0
  5. uv.lock +62 -84
app.py CHANGED
@@ -6,11 +6,13 @@ os.environ.setdefault("MPLBACKEND", "Agg")
6
  import re
7
  import tempfile
8
  import time
 
9
  from functools import lru_cache
10
  from pathlib import Path
11
 
12
  import gradio as gr
13
  from geopy.geocoders import Nominatim
 
14
 
15
  import osmnx as ox
16
 
@@ -21,11 +23,90 @@ APP_TITLE = "MapToPoster"
21
  DEFAULT_DISTANCE_M = 10000
22
  MIN_DISTANCE_M = 2000
23
  MAX_DISTANCE_M = 20000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
 
26
  _REPO_ROOT = Path(__file__).resolve().parent
27
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  def _load_readme_example_posters() -> list[tuple[str, str]]:
30
  """Return (absolute_path, caption) pairs for example posters referenced in README."""
31
 
@@ -107,48 +188,70 @@ def _rate_limit_geocode(min_interval_s: float = 1.05) -> None:
107
  _last_geocode_ts = time.time()
108
 
109
 
 
110
  def _geocoder() -> Nominatim:
111
  user_agent = os.environ.get("MAPTOP_POSTER_USER_AGENT", "maptoposter-hf-space")
112
  return Nominatim(user_agent=user_agent)
113
 
114
 
115
  @lru_cache(maxsize=256)
116
- def _geocode(city: str, country: str) -> tuple[float, float]:
117
  _rate_limit_geocode()
118
  location = _geocoder().geocode(f"{city}, {country}")
119
  if not location:
120
  raise ValueError(f"Could not find coordinates for {city}, {country}")
121
- return (float(location.latitude), float(location.longitude))
122
-
123
-
124
- def generate(city: str, country: str, theme: str, distance_m: int) -> str:
125
- city = (city or "").strip()
126
- country = (country or "").strip()
127
 
128
- if not city or not country:
129
- raise gr.Error("City and Country are required.")
130
 
131
- distance_m = int(distance_m)
132
- if distance_m < MIN_DISTANCE_M or distance_m > MAX_DISTANCE_M:
133
- raise gr.Error(
134
- f"Distance must be between {MIN_DISTANCE_M} and {MAX_DISTANCE_M} meters."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  )
 
 
136
 
137
  available_themes = maptoposter.get_available_themes()
138
- if theme not in available_themes:
139
- raise gr.Error(f"Unknown theme: {theme}")
140
 
141
- maptoposter.THEME = maptoposter.load_theme(theme)
142
 
143
- lat, lon = _geocode(city, country)
144
 
145
  tmp_dir = tempfile.gettempdir()
146
  output_path = os.path.join(
147
  tmp_dir,
148
- f"{_slugify(city)}_{_slugify(theme)}_{int(time.time())}.png",
149
  )
150
 
151
- maptoposter.create_poster(city, country, (lat, lon), distance_m, output_path)
 
 
 
 
 
 
 
 
 
152
  return output_path
153
 
154
 
@@ -304,6 +407,26 @@ def build_demo() -> gr.Blocks:
304
  value=DEFAULT_DISTANCE_M,
305
  )
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  btn = gr.Button("Generate poster", elem_classes=["mtp-primary"])
308
  gr.HTML(
309
  """<div class="mtp-subtle">Uses public geocoding + OSM services. Please keep distances modest.</div>"""
@@ -313,7 +436,11 @@ def build_demo() -> gr.Blocks:
313
  with gr.Group(elem_classes=["mtp-card"]):
314
  out = gr.Image(label="Poster", type="filepath", show_label=True)
315
 
316
- btn.click(generate, inputs=[city, country, theme, distance], outputs=[out])
 
 
 
 
317
 
318
  if example_posters:
319
  gr.Markdown("## Example gallery")
 
6
  import re
7
  import tempfile
8
  import time
9
+ from enum import Enum
10
  from functools import lru_cache
11
  from pathlib import Path
12
 
13
  import gradio as gr
14
  from geopy.geocoders import Nominatim
15
+ from pydantic import BaseModel, ValidationError, field_validator
16
 
17
  import osmnx as ox
18
 
 
23
  DEFAULT_DISTANCE_M = 10000
24
  MIN_DISTANCE_M = 2000
25
  MAX_DISTANCE_M = 20000
26
+ DEFAULT_DPI = 300
27
+ MIN_DPI = 150
28
+ MAX_DPI = 600
29
+
30
+
31
+ class NetworkType(str, Enum):
32
+ ALL = "all"
33
+ ALL_PUBLIC = "all_public"
34
+ DRIVE = "drive"
35
+ DRIVE_SERVICE = "drive_service"
36
+ WALK = "walk"
37
+ BIKE = "bike"
38
+
39
+
40
+ class DistanceType(str, Enum):
41
+ BBOX = "bbox"
42
+ NETWORK = "network"
43
+
44
+
45
+ NETWORK_TYPES = [item.value for item in NetworkType]
46
+ DIST_TYPES = [item.value for item in DistanceType]
47
 
48
 
49
  _REPO_ROOT = Path(__file__).resolve().parent
50
 
51
 
52
+ class GenerateRequest(BaseModel):
53
+ city: str
54
+ country: str
55
+ theme: str
56
+ distance_m: int
57
+ dpi: int
58
+ network_type: NetworkType
59
+ dist_type: DistanceType
60
+
61
+ @field_validator("city", "country", "theme")
62
+ @classmethod
63
+ def _strip_and_require(cls, value: str) -> str:
64
+ value = (value or "").strip()
65
+ if not value:
66
+ raise ValueError("must be provided")
67
+ return value
68
+
69
+ @field_validator("distance_m")
70
+ @classmethod
71
+ def _validate_distance(cls, value: int) -> int:
72
+ value = int(value)
73
+ if value < MIN_DISTANCE_M or value > MAX_DISTANCE_M:
74
+ raise ValueError(
75
+ f"Distance must be between {MIN_DISTANCE_M} and {MAX_DISTANCE_M} meters."
76
+ )
77
+ return value
78
+
79
+ @field_validator("dpi")
80
+ @classmethod
81
+ def _validate_dpi(cls, value: int) -> int:
82
+ value = int(value)
83
+ if value < MIN_DPI or value > MAX_DPI:
84
+ raise ValueError(f"DPI must be between {MIN_DPI} and {MAX_DPI}.")
85
+ return value
86
+
87
+ @field_validator("network_type", mode="before")
88
+ @classmethod
89
+ def _validate_network_type(cls, value: str | NetworkType) -> NetworkType:
90
+ if isinstance(value, NetworkType):
91
+ return value
92
+ value = (value or "").strip()
93
+ try:
94
+ return NetworkType(value)
95
+ except ValueError as exc:
96
+ raise ValueError("Invalid network type.") from exc
97
+
98
+ @field_validator("dist_type", mode="before")
99
+ @classmethod
100
+ def _validate_dist_type(cls, value: str | DistanceType) -> DistanceType:
101
+ if isinstance(value, DistanceType):
102
+ return value
103
+ value = (value or "").strip()
104
+ try:
105
+ return DistanceType(value)
106
+ except ValueError as exc:
107
+ raise ValueError("Invalid distance type.") from exc
108
+
109
+
110
  def _load_readme_example_posters() -> list[tuple[str, str]]:
111
  """Return (absolute_path, caption) pairs for example posters referenced in README."""
112
 
 
188
  _last_geocode_ts = time.time()
189
 
190
 
191
+ @lru_cache(maxsize=1)
192
  def _geocoder() -> Nominatim:
193
  user_agent = os.environ.get("MAPTOP_POSTER_USER_AGENT", "maptoposter-hf-space")
194
  return Nominatim(user_agent=user_agent)
195
 
196
 
197
  @lru_cache(maxsize=256)
198
+ def _geocode(city: str, country: str) -> maptoposter.Coordinates:
199
  _rate_limit_geocode()
200
  location = _geocoder().geocode(f"{city}, {country}")
201
  if not location:
202
  raise ValueError(f"Could not find coordinates for {city}, {country}")
203
+ return maptoposter.Coordinates(
204
+ lat=float(location.latitude),
205
+ lon=float(location.longitude),
206
+ )
 
 
207
 
 
 
208
 
209
+ def generate(
210
+ city: str,
211
+ country: str,
212
+ theme: str,
213
+ distance_m: int,
214
+ dpi: int,
215
+ network_type: str,
216
+ dist_type: str,
217
+ ) -> str:
218
+ try:
219
+ request = GenerateRequest(
220
+ city=city,
221
+ country=country,
222
+ theme=theme,
223
+ distance_m=distance_m,
224
+ dpi=dpi,
225
+ network_type=network_type,
226
+ dist_type=dist_type,
227
  )
228
+ except ValidationError as exc:
229
+ raise gr.Error(str(exc))
230
 
231
  available_themes = maptoposter.get_available_themes()
232
+ if request.theme not in available_themes:
233
+ raise gr.Error(f"Unknown theme: {request.theme}")
234
 
235
+ maptoposter.THEME = maptoposter.load_theme(request.theme)
236
 
237
+ coords = _geocode(request.city, request.country)
238
 
239
  tmp_dir = tempfile.gettempdir()
240
  output_path = os.path.join(
241
  tmp_dir,
242
+ f"{_slugify(request.city)}_{_slugify(request.theme)}_{int(time.time())}.png",
243
  )
244
 
245
+ maptoposter.create_poster(
246
+ request.city,
247
+ request.country,
248
+ coords,
249
+ request.distance_m,
250
+ output_path,
251
+ network_type=request.network_type.value,
252
+ dist_type=request.dist_type.value,
253
+ dpi=request.dpi,
254
+ )
255
  return output_path
256
 
257
 
 
407
  value=DEFAULT_DISTANCE_M,
408
  )
409
 
410
+ with gr.Accordion("Advanced settings", open=False):
411
+ with gr.Row():
412
+ dpi = gr.Slider(
413
+ label="DPI",
414
+ minimum=MIN_DPI,
415
+ maximum=MAX_DPI,
416
+ step=10,
417
+ value=DEFAULT_DPI,
418
+ )
419
+ network_type = gr.Dropdown(
420
+ label="Network type",
421
+ choices=NETWORK_TYPES,
422
+ value="all",
423
+ )
424
+ dist_type = gr.Dropdown(
425
+ label="Distance type",
426
+ choices=DIST_TYPES,
427
+ value="bbox",
428
+ )
429
+
430
  btn = gr.Button("Generate poster", elem_classes=["mtp-primary"])
431
  gr.HTML(
432
  """<div class="mtp-subtle">Uses public geocoding + OSM services. Please keep distances modest.</div>"""
 
436
  with gr.Group(elem_classes=["mtp-card"]):
437
  out = gr.Image(label="Poster", type="filepath", show_label=True)
438
 
439
+ btn.click(
440
+ generate,
441
+ inputs=[city, country, theme, distance, dpi, network_type, dist_type],
442
+ outputs=[out],
443
+ )
444
 
445
  if example_posters:
446
  gr.Markdown("## Example gallery")
create_map_poster.py CHANGED
@@ -8,35 +8,98 @@ from tqdm import tqdm
8
  import time
9
  import json
10
  import os
 
11
  from datetime import datetime
12
  import argparse
 
 
 
 
13
 
14
  THEMES_DIR = "themes"
15
  FONTS_DIR = "fonts"
16
  POSTERS_DIR = "posters"
17
 
18
- def load_fonts():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  """
20
  Load Roboto fonts from the fonts directory.
21
- Returns dict with font paths for different weights.
22
  """
23
- fonts = {
24
- 'bold': os.path.join(FONTS_DIR, 'Roboto-Bold.ttf'),
25
- 'regular': os.path.join(FONTS_DIR, 'Roboto-Regular.ttf'),
26
- 'light': os.path.join(FONTS_DIR, 'Roboto-Light.ttf')
27
- }
28
-
29
- # Verify fonts exist
30
- for weight, path in fonts.items():
31
  if not os.path.exists(path):
32
  print(f"⚠ Font not found: {path}")
33
  return None
34
-
35
  return fonts
36
 
37
  FONTS = load_fonts()
38
 
39
- def generate_output_filename(city, theme_name):
 
 
 
 
 
 
40
  """
41
  Generate unique output filename with city, theme, and datetime.
42
  """
@@ -48,7 +111,7 @@ def generate_output_filename(city, theme_name):
48
  filename = f"{city_slug}_{theme_name}_{timestamp}.png"
49
  return os.path.join(POSTERS_DIR, filename)
50
 
51
- def get_available_themes():
52
  """
53
  Scans the themes directory and returns a list of available theme names.
54
  """
@@ -63,7 +126,7 @@ def get_available_themes():
63
  themes.append(theme_name)
64
  return themes
65
 
66
- def load_theme(theme_name="feature_based"):
67
  """
68
  Load theme from JSON file in themes directory.
69
  """
@@ -71,34 +134,25 @@ def load_theme(theme_name="feature_based"):
71
 
72
  if not os.path.exists(theme_file):
73
  print(f"⚠ Theme file '{theme_file}' not found. Using default feature_based theme.")
74
- # Fallback to embedded default theme
75
- return {
76
- "name": "Feature-Based Shading",
77
- "bg": "#FFFFFF",
78
- "text": "#000000",
79
- "gradient_color": "#FFFFFF",
80
- "water": "#C0C0C0",
81
- "parks": "#F0F0F0",
82
- "road_motorway": "#0A0A0A",
83
- "road_primary": "#1A1A1A",
84
- "road_secondary": "#2A2A2A",
85
- "road_tertiary": "#3A3A3A",
86
- "road_residential": "#4A4A4A",
87
- "road_default": "#3A3A3A"
88
- }
89
 
90
- with open(theme_file, 'r') as f:
91
- theme = json.load(f)
92
- print(f"✓ Loaded theme: {theme.get('name', theme_name)}")
93
- if 'description' in theme:
94
- print(f" {theme['description']}")
 
 
 
 
 
95
  return theme
96
 
97
  # Load theme (can be changed via command line or input)
98
- THEME = None # Will be loaded later
99
 
100
 
101
- def main(argv=None):
102
  parser = argparse.ArgumentParser(
103
  description="Generate beautiful map posters for any city",
104
  formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -152,9 +206,15 @@ Examples:
152
 
153
  # Get coordinates and generate poster
154
  try:
155
- coords = get_coordinates(args.city, args.country)
156
- output_file = generate_output_filename(args.city, args.theme)
157
- create_poster(args.city, args.country, coords, args.distance, output_file)
 
 
 
 
 
 
158
 
159
  print("\n" + "=" * 50)
160
  print("✓ Poster generation complete!")
@@ -167,7 +227,7 @@ Examples:
167
  traceback.print_exc()
168
  return 1
169
 
170
- def create_gradient_fade(ax, color, location='bottom', zorder=10):
171
  """
172
  Creates a fade effect at the top or bottom of the map.
173
  """
@@ -201,12 +261,19 @@ def create_gradient_fade(ax, color, location='bottom', zorder=10):
201
  ax.imshow(gradient, extent=[xlim[0], xlim[1], y_bottom, y_top],
202
  aspect='auto', cmap=custom_cmap, zorder=zorder, origin='lower')
203
 
204
- def get_edge_colors_by_type(G):
 
 
 
 
 
 
205
  """
206
  Assigns colors to edges based on road type hierarchy.
207
  Returns a list of colors corresponding to each edge in the graph.
208
  """
209
- edge_colors = []
 
210
 
211
  for u, v, data in G.edges(data=True):
212
  # Get the highway type (can be a list or string)
@@ -217,29 +284,29 @@ def get_edge_colors_by_type(G):
217
  highway = highway[0] if highway else 'unclassified'
218
 
219
  # Assign color based on road type
220
- if highway in ['motorway', 'motorway_link']:
221
- color = THEME['road_motorway']
222
- elif highway in ['trunk', 'trunk_link', 'primary', 'primary_link']:
223
- color = THEME['road_primary']
224
- elif highway in ['secondary', 'secondary_link']:
225
- color = THEME['road_secondary']
226
- elif highway in ['tertiary', 'tertiary_link']:
227
- color = THEME['road_tertiary']
228
- elif highway in ['residential', 'living_street', 'unclassified']:
229
- color = THEME['road_residential']
230
  else:
231
- color = THEME['road_default']
232
 
233
  edge_colors.append(color)
234
 
235
  return edge_colors
236
 
237
- def get_edge_widths_by_type(G):
238
  """
239
  Assigns line widths to edges based on road type.
240
  Major roads get thicker lines.
241
  """
242
- edge_widths = []
243
 
244
  for u, v, data in G.edges(data=True):
245
  highway = data.get('highway', 'unclassified')
@@ -263,14 +330,15 @@ def get_edge_widths_by_type(G):
263
 
264
  return edge_widths
265
 
266
- def get_coordinates(city, country):
 
267
  """
268
  Fetches coordinates for a given city and country using geopy.
269
  Includes rate limiting to be respectful to the geocoding service.
270
  """
271
  print("Looking up coordinates...")
272
- geolocator = Nominatim(user_agent="city_map_poster")
273
-
274
  # Add a small delay to respect Nominatim's usage policy
275
  time.sleep(1)
276
 
@@ -279,25 +347,50 @@ def get_coordinates(city, country):
279
  if location:
280
  print(f"✓ Found: {location.address}")
281
  print(f"✓ Coordinates: {location.latitude}, {location.longitude}")
282
- return (location.latitude, location.longitude)
283
  else:
284
  raise ValueError(f"Could not find coordinates for {city}, {country}")
285
 
286
- def create_poster(city, country, point, dist, output_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  print(f"\nGenerating map for {city}, {country}...")
 
 
288
 
289
  # Progress bar for data fetching
290
  with tqdm(total=3, desc="Fetching map data", unit="step", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') as pbar:
291
  # 1. Fetch Street Network
292
  pbar.set_description("Downloading street network")
293
- G = ox.graph_from_point(point, dist=dist, dist_type='bbox', network_type='all')
 
 
 
 
 
294
  pbar.update(1)
295
  time.sleep(0.5) # Rate limit between requests
296
 
297
  # 2. Fetch Water Features
298
  pbar.set_description("Downloading water features")
299
  try:
300
- water = ox.features_from_point(point, tags={'natural': 'water', 'waterway': 'riverbank'}, dist=dist)
301
  except:
302
  water = None
303
  pbar.update(1)
@@ -306,7 +399,7 @@ def create_poster(city, country, point, dist, output_file):
306
  # 3. Fetch Parks
307
  pbar.set_description("Downloading parks/green spaces")
308
  try:
309
- parks = ox.features_from_point(point, tags={'leisure': 'park', 'landuse': 'grass'}, dist=dist)
310
  except:
311
  parks = None
312
  pbar.update(1)
@@ -315,16 +408,16 @@ def create_poster(city, country, point, dist, output_file):
315
 
316
  # 2. Setup Plot
317
  print("Rendering map...")
318
- fig, ax = plt.subplots(figsize=(12, 16), facecolor=THEME['bg'])
319
- ax.set_facecolor(THEME['bg'])
320
  ax.set_position([0, 0, 1, 1])
321
 
322
  # 3. Plot Layers
323
  # Layer 1: Polygons
324
  if water is not None and not water.empty:
325
- water.plot(ax=ax, facecolor=THEME['water'], edgecolor='none', zorder=1)
326
  if parks is not None and not parks.empty:
327
- parks.plot(ax=ax, facecolor=THEME['parks'], edgecolor='none', zorder=2)
328
 
329
  # Layer 2: Roads with hierarchy coloring
330
  print("Applying road hierarchy colors...")
@@ -332,7 +425,7 @@ def create_poster(city, country, point, dist, output_file):
332
  edge_widths = get_edge_widths_by_type(G)
333
 
334
  ox.plot_graph(
335
- G, ax=ax, bgcolor=THEME['bg'],
336
  node_size=0,
337
  edge_color=edge_colors,
338
  edge_linewidth=edge_widths,
@@ -340,15 +433,15 @@ def create_poster(city, country, point, dist, output_file):
340
  )
341
 
342
  # Layer 3: Gradients (Top and Bottom)
343
- create_gradient_fade(ax, THEME['gradient_color'], location='bottom', zorder=10)
344
- create_gradient_fade(ax, THEME['gradient_color'], location='top', zorder=10)
345
 
346
  # 4. Typography using Roboto font
347
  if FONTS:
348
- font_main = FontProperties(fname=FONTS['bold'], size=60)
349
- font_top = FontProperties(fname=FONTS['bold'], size=40)
350
- font_sub = FontProperties(fname=FONTS['light'], size=22)
351
- font_coords = FontProperties(fname=FONTS['regular'], size=14)
352
  else:
353
  # Fallback to system fonts
354
  font_main = FontProperties(family='monospace', weight='bold', size=60)
@@ -360,35 +453,35 @@ def create_poster(city, country, point, dist, output_file):
360
 
361
  # --- BOTTOM TEXT ---
362
  ax.text(0.5, 0.14, spaced_city, transform=ax.transAxes,
363
- color=THEME['text'], ha='center', fontproperties=font_main, zorder=11)
364
 
365
  ax.text(0.5, 0.10, country.upper(), transform=ax.transAxes,
366
- color=THEME['text'], ha='center', fontproperties=font_sub, zorder=11)
367
 
368
- lat, lon = point
369
  coords = f"{lat:.4f}° N / {lon:.4f}° E" if lat >= 0 else f"{abs(lat):.4f}° S / {lon:.4f}° E"
370
  if lon < 0:
371
  coords = coords.replace("E", "W")
372
 
373
  ax.text(0.5, 0.07, coords, transform=ax.transAxes,
374
- color=THEME['text'], alpha=0.7, ha='center', fontproperties=font_coords, zorder=11)
375
 
376
  ax.plot([0.4, 0.6], [0.125, 0.125], transform=ax.transAxes,
377
- color=THEME['text'], linewidth=1, zorder=11)
378
 
379
  # --- ATTRIBUTION (bottom right) ---
380
  if FONTS:
381
- font_attr = FontProperties(fname=FONTS['light'], size=8)
382
  else:
383
  font_attr = FontProperties(family='monospace', size=8)
384
 
385
  ax.text(0.98, 0.02, "© OpenStreetMap contributors", transform=ax.transAxes,
386
- color=THEME['text'], alpha=0.5, ha='right', va='bottom',
387
  fontproperties=font_attr, zorder=11)
388
 
389
  # 5. Save
390
  print(f"Saving to {output_file}...")
391
- plt.savefig(output_file, dpi=300, facecolor=THEME['bg'])
392
  plt.close()
393
  print(f"✓ Done! Poster saved as {output_file}")
394
 
@@ -460,13 +553,14 @@ def list_themes():
460
  for theme_name in available_themes:
461
  theme_path = os.path.join(THEMES_DIR, f"{theme_name}.json")
462
  try:
463
- with open(theme_path, 'r') as f:
464
  theme_data = json.load(f)
465
- display_name = theme_data.get('name', theme_name)
466
- description = theme_data.get('description', '')
467
- except:
 
468
  display_name = theme_name
469
- description = ''
470
  print(f" {theme_name}")
471
  print(f" {display_name}")
472
  if description:
 
8
  import time
9
  import json
10
  import os
11
+ from dataclasses import dataclass
12
  from datetime import datetime
13
  import argparse
14
+ from functools import lru_cache
15
+ from typing import Optional, Sequence
16
+
17
+ from pydantic import BaseModel, ConfigDict, ValidationError
18
 
19
  THEMES_DIR = "themes"
20
  FONTS_DIR = "fonts"
21
  POSTERS_DIR = "posters"
22
 
23
+
24
+ def _configure_osmnx_cache() -> None:
25
+ cache_dir = os.environ.get("OSMNX_CACHE_DIR", "/tmp/osmnx_cache")
26
+ os.makedirs(cache_dir, exist_ok=True)
27
+ ox.settings.use_cache = True
28
+ ox.settings.cache_folder = cache_dir
29
+ ox.settings.log_console = False
30
+
31
+
32
+ _configure_osmnx_cache()
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class FontPaths:
37
+ bold: str
38
+ regular: str
39
+ light: str
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Coordinates:
44
+ lat: float
45
+ lon: float
46
+
47
+ def as_tuple(self) -> tuple[float, float]:
48
+ return (self.lat, self.lon)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class PosterRequest:
53
+ city: str
54
+ country: str
55
+ theme: str
56
+ distance_m: int
57
+
58
+
59
+ class ThemeConfig(BaseModel):
60
+ model_config = ConfigDict(extra="ignore")
61
+
62
+ name: str = "Feature-Based Shading"
63
+ description: Optional[str] = None
64
+ bg: str = "#FFFFFF"
65
+ text: str = "#000000"
66
+ gradient_color: str = "#FFFFFF"
67
+ water: str = "#C0C0C0"
68
+ parks: str = "#F0F0F0"
69
+ road_motorway: str = "#0A0A0A"
70
+ road_primary: str = "#1A1A1A"
71
+ road_secondary: str = "#2A2A2A"
72
+ road_tertiary: str = "#3A3A3A"
73
+ road_residential: str = "#4A4A4A"
74
+ road_default: str = "#3A3A3A"
75
+
76
+ def load_fonts() -> Optional[FontPaths]:
77
  """
78
  Load Roboto fonts from the fonts directory.
79
+ Returns FontPaths for different weights.
80
  """
81
+ fonts = FontPaths(
82
+ bold=os.path.join(FONTS_DIR, "Roboto-Bold.ttf"),
83
+ regular=os.path.join(FONTS_DIR, "Roboto-Regular.ttf"),
84
+ light=os.path.join(FONTS_DIR, "Roboto-Light.ttf"),
85
+ )
86
+
87
+ for path in (fonts.bold, fonts.regular, fonts.light):
 
88
  if not os.path.exists(path):
89
  print(f"⚠ Font not found: {path}")
90
  return None
91
+
92
  return fonts
93
 
94
  FONTS = load_fonts()
95
 
96
+
97
+ @lru_cache(maxsize=1)
98
+ def _geocoder() -> Nominatim:
99
+ user_agent = os.environ.get("MAPTOP_POSTER_USER_AGENT", "maptoposter-cli")
100
+ return Nominatim(user_agent=user_agent)
101
+
102
+ def generate_output_filename(city: str, theme_name: str) -> str:
103
  """
104
  Generate unique output filename with city, theme, and datetime.
105
  """
 
111
  filename = f"{city_slug}_{theme_name}_{timestamp}.png"
112
  return os.path.join(POSTERS_DIR, filename)
113
 
114
+ def get_available_themes() -> list[str]:
115
  """
116
  Scans the themes directory and returns a list of available theme names.
117
  """
 
126
  themes.append(theme_name)
127
  return themes
128
 
129
+ def load_theme(theme_name: str = "feature_based") -> ThemeConfig:
130
  """
131
  Load theme from JSON file in themes directory.
132
  """
 
134
 
135
  if not os.path.exists(theme_file):
136
  print(f"⚠ Theme file '{theme_file}' not found. Using default feature_based theme.")
137
+ return ThemeConfig()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ with open(theme_file, "r") as f:
140
+ raw_theme = json.load(f)
141
+ try:
142
+ theme = ThemeConfig(**raw_theme)
143
+ except ValidationError as exc:
144
+ print(f"⚠ Theme file '{theme_file}' is invalid: {exc}")
145
+ theme = ThemeConfig()
146
+ print(f"✓ Loaded theme: {theme.name or theme_name}")
147
+ if theme.description:
148
+ print(f" {theme.description}")
149
  return theme
150
 
151
  # Load theme (can be changed via command line or input)
152
+ THEME: Optional[ThemeConfig] = None # Will be loaded later
153
 
154
 
155
+ def main(argv: Optional[Sequence[str]] = None) -> int:
156
  parser = argparse.ArgumentParser(
157
  description="Generate beautiful map posters for any city",
158
  formatter_class=argparse.RawDescriptionHelpFormatter,
 
206
 
207
  # Get coordinates and generate poster
208
  try:
209
+ request = PosterRequest(
210
+ city=args.city,
211
+ country=args.country,
212
+ theme=args.theme,
213
+ distance_m=args.distance,
214
+ )
215
+ coords = get_coordinates(request.city, request.country)
216
+ output_file = generate_output_filename(request.city, request.theme)
217
+ create_poster(request.city, request.country, coords, request.distance_m, output_file)
218
 
219
  print("\n" + "=" * 50)
220
  print("✓ Poster generation complete!")
 
227
  traceback.print_exc()
228
  return 1
229
 
230
+ def create_gradient_fade(ax, color: str, location: str = "bottom", zorder: int = 10) -> None:
231
  """
232
  Creates a fade effect at the top or bottom of the map.
233
  """
 
261
  ax.imshow(gradient, extent=[xlim[0], xlim[1], y_bottom, y_top],
262
  aspect='auto', cmap=custom_cmap, zorder=zorder, origin='lower')
263
 
264
+ def _require_theme() -> ThemeConfig:
265
+ if THEME is None:
266
+ raise RuntimeError("Theme is not loaded. Call load_theme() first.")
267
+ return THEME
268
+
269
+
270
+ def get_edge_colors_by_type(G) -> list[str]:
271
  """
272
  Assigns colors to edges based on road type hierarchy.
273
  Returns a list of colors corresponding to each edge in the graph.
274
  """
275
+ theme = _require_theme()
276
+ edge_colors: list[str] = []
277
 
278
  for u, v, data in G.edges(data=True):
279
  # Get the highway type (can be a list or string)
 
284
  highway = highway[0] if highway else 'unclassified'
285
 
286
  # Assign color based on road type
287
+ if highway in ["motorway", "motorway_link"]:
288
+ color = theme.road_motorway
289
+ elif highway in ["trunk", "trunk_link", "primary", "primary_link"]:
290
+ color = theme.road_primary
291
+ elif highway in ["secondary", "secondary_link"]:
292
+ color = theme.road_secondary
293
+ elif highway in ["tertiary", "tertiary_link"]:
294
+ color = theme.road_tertiary
295
+ elif highway in ["residential", "living_street", "unclassified"]:
296
+ color = theme.road_residential
297
  else:
298
+ color = theme.road_default
299
 
300
  edge_colors.append(color)
301
 
302
  return edge_colors
303
 
304
+ def get_edge_widths_by_type(G) -> list[float]:
305
  """
306
  Assigns line widths to edges based on road type.
307
  Major roads get thicker lines.
308
  """
309
+ edge_widths: list[float] = []
310
 
311
  for u, v, data in G.edges(data=True):
312
  highway = data.get('highway', 'unclassified')
 
330
 
331
  return edge_widths
332
 
333
+ @lru_cache(maxsize=256)
334
+ def get_coordinates(city: str, country: str) -> Coordinates:
335
  """
336
  Fetches coordinates for a given city and country using geopy.
337
  Includes rate limiting to be respectful to the geocoding service.
338
  """
339
  print("Looking up coordinates...")
340
+ geolocator = _geocoder()
341
+
342
  # Add a small delay to respect Nominatim's usage policy
343
  time.sleep(1)
344
 
 
347
  if location:
348
  print(f"✓ Found: {location.address}")
349
  print(f"✓ Coordinates: {location.latitude}, {location.longitude}")
350
+ return Coordinates(lat=float(location.latitude), lon=float(location.longitude))
351
  else:
352
  raise ValueError(f"Could not find coordinates for {city}, {country}")
353
 
354
+ def _coerce_coordinates(point: Coordinates | Sequence[float]) -> Coordinates:
355
+ if isinstance(point, Coordinates):
356
+ return point
357
+ if isinstance(point, Sequence) and len(point) == 2:
358
+ return Coordinates(lat=float(point[0]), lon=float(point[1]))
359
+ raise TypeError("point must be Coordinates or (lat, lon) sequence")
360
+
361
+
362
+ def create_poster(
363
+ city: str,
364
+ country: str,
365
+ point: Coordinates | Sequence[float],
366
+ dist: int,
367
+ output_file: str,
368
+ *,
369
+ network_type: str = "all",
370
+ dist_type: str = "bbox",
371
+ dpi: int = 300,
372
+ ) -> None:
373
  print(f"\nGenerating map for {city}, {country}...")
374
+ theme = _require_theme()
375
+ coords = _coerce_coordinates(point)
376
 
377
  # Progress bar for data fetching
378
  with tqdm(total=3, desc="Fetching map data", unit="step", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}') as pbar:
379
  # 1. Fetch Street Network
380
  pbar.set_description("Downloading street network")
381
+ G = ox.graph_from_point(
382
+ coords.as_tuple(),
383
+ dist=dist,
384
+ dist_type=dist_type,
385
+ network_type=network_type,
386
+ )
387
  pbar.update(1)
388
  time.sleep(0.5) # Rate limit between requests
389
 
390
  # 2. Fetch Water Features
391
  pbar.set_description("Downloading water features")
392
  try:
393
+ water = ox.features_from_point(coords.as_tuple(), tags={"natural": "water", "waterway": "riverbank"}, dist=dist)
394
  except:
395
  water = None
396
  pbar.update(1)
 
399
  # 3. Fetch Parks
400
  pbar.set_description("Downloading parks/green spaces")
401
  try:
402
+ parks = ox.features_from_point(coords.as_tuple(), tags={"leisure": "park", "landuse": "grass"}, dist=dist)
403
  except:
404
  parks = None
405
  pbar.update(1)
 
408
 
409
  # 2. Setup Plot
410
  print("Rendering map...")
411
+ fig, ax = plt.subplots(figsize=(12, 16), facecolor=theme.bg)
412
+ ax.set_facecolor(theme.bg)
413
  ax.set_position([0, 0, 1, 1])
414
 
415
  # 3. Plot Layers
416
  # Layer 1: Polygons
417
  if water is not None and not water.empty:
418
+ water.plot(ax=ax, facecolor=theme.water, edgecolor="none", zorder=1)
419
  if parks is not None and not parks.empty:
420
+ parks.plot(ax=ax, facecolor=theme.parks, edgecolor="none", zorder=2)
421
 
422
  # Layer 2: Roads with hierarchy coloring
423
  print("Applying road hierarchy colors...")
 
425
  edge_widths = get_edge_widths_by_type(G)
426
 
427
  ox.plot_graph(
428
+ G, ax=ax, bgcolor=theme.bg,
429
  node_size=0,
430
  edge_color=edge_colors,
431
  edge_linewidth=edge_widths,
 
433
  )
434
 
435
  # Layer 3: Gradients (Top and Bottom)
436
+ create_gradient_fade(ax, theme.gradient_color, location="bottom", zorder=10)
437
+ create_gradient_fade(ax, theme.gradient_color, location="top", zorder=10)
438
 
439
  # 4. Typography using Roboto font
440
  if FONTS:
441
+ font_main = FontProperties(fname=FONTS.bold, size=60)
442
+ font_top = FontProperties(fname=FONTS.bold, size=40)
443
+ font_sub = FontProperties(fname=FONTS.light, size=22)
444
+ font_coords = FontProperties(fname=FONTS.regular, size=14)
445
  else:
446
  # Fallback to system fonts
447
  font_main = FontProperties(family='monospace', weight='bold', size=60)
 
453
 
454
  # --- BOTTOM TEXT ---
455
  ax.text(0.5, 0.14, spaced_city, transform=ax.transAxes,
456
+ color=theme.text, ha="center", fontproperties=font_main, zorder=11)
457
 
458
  ax.text(0.5, 0.10, country.upper(), transform=ax.transAxes,
459
+ color=theme.text, ha="center", fontproperties=font_sub, zorder=11)
460
 
461
+ lat, lon = coords.lat, coords.lon
462
  coords = f"{lat:.4f}° N / {lon:.4f}° E" if lat >= 0 else f"{abs(lat):.4f}° S / {lon:.4f}° E"
463
  if lon < 0:
464
  coords = coords.replace("E", "W")
465
 
466
  ax.text(0.5, 0.07, coords, transform=ax.transAxes,
467
+ color=theme.text, alpha=0.7, ha="center", fontproperties=font_coords, zorder=11)
468
 
469
  ax.plot([0.4, 0.6], [0.125, 0.125], transform=ax.transAxes,
470
+ color=theme.text, linewidth=1, zorder=11)
471
 
472
  # --- ATTRIBUTION (bottom right) ---
473
  if FONTS:
474
+ font_attr = FontProperties(fname=FONTS.light, size=8)
475
  else:
476
  font_attr = FontProperties(family='monospace', size=8)
477
 
478
  ax.text(0.98, 0.02, "© OpenStreetMap contributors", transform=ax.transAxes,
479
+ color=theme.text, alpha=0.5, ha="right", va="bottom",
480
  fontproperties=font_attr, zorder=11)
481
 
482
  # 5. Save
483
  print(f"Saving to {output_file}...")
484
+ plt.savefig(output_file, dpi=int(dpi), facecolor=theme.bg)
485
  plt.close()
486
  print(f"✓ Done! Poster saved as {output_file}")
487
 
 
553
  for theme_name in available_themes:
554
  theme_path = os.path.join(THEMES_DIR, f"{theme_name}.json")
555
  try:
556
+ with open(theme_path, "r") as f:
557
  theme_data = json.load(f)
558
+ theme = ThemeConfig(**theme_data)
559
+ display_name = theme.name or theme_name
560
+ description = theme.description or ""
561
+ except Exception:
562
  display_name = theme_name
563
+ description = ""
564
  print(f" {theme_name}")
565
  print(f" {display_name}")
566
  if description:
pyproject.toml CHANGED
@@ -32,6 +32,7 @@ dependencies = [
32
  "packaging==25.0",
33
  "pandas==2.3.3",
34
  "pillow==12.1.0",
 
35
  "pyogrio==0.12.1",
36
  "pyparsing==3.3.1",
37
  "pyproj==3.7.2",
 
32
  "packaging==25.0",
33
  "pandas==2.3.3",
34
  "pillow==12.1.0",
35
+ "pydantic==2.11.1",
36
  "pyogrio==0.12.1",
37
  "pyparsing==3.3.1",
38
  "pyproj==3.7.2",
requirements.txt CHANGED
@@ -21,6 +21,7 @@ osmnx==2.0.7
21
  packaging==25.0
22
  pandas==2.3.3
23
  pillow==12.1.0
 
24
  pyogrio==0.12.1
25
  pyparsing==3.3.1
26
  pyproj==3.7.2
 
21
  packaging==25.0
22
  pandas==2.3.3
23
  pillow==12.1.0
24
+ pydantic==2.11.1
25
  pyogrio==0.12.1
26
  pyparsing==3.3.1
27
  pyproj==3.7.2
uv.lock CHANGED
@@ -758,6 +758,7 @@ dependencies = [
758
  { name = "packaging" },
759
  { name = "pandas" },
760
  { name = "pillow" },
 
761
  { name = "pyogrio" },
762
  { name = "pyparsing" },
763
  { name = "pyproj" },
@@ -792,6 +793,7 @@ requires-dist = [
792
  { name = "packaging", specifier = "==25.0" },
793
  { name = "pandas", specifier = "==2.3.3" },
794
  { name = "pillow", specifier = "==12.1.0" },
 
795
  { name = "pyogrio", specifier = "==0.12.1" },
796
  { name = "pyparsing", specifier = "==3.3.1" },
797
  { name = "pyproj", specifier = "==3.7.2" },
@@ -1290,7 +1292,7 @@ wheels = [
1290
 
1291
  [[package]]
1292
  name = "pydantic"
1293
- version = "2.12.5"
1294
  source = { registry = "https://pypi.org/simple" }
1295
  dependencies = [
1296
  { name = "annotated-types" },
@@ -1298,98 +1300,74 @@ dependencies = [
1298
  { name = "typing-extensions" },
1299
  { name = "typing-inspection" },
1300
  ]
1301
- sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 }
1302
  wheels = [
1303
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 },
1304
  ]
1305
 
1306
  [[package]]
1307
  name = "pydantic-core"
1308
- version = "2.41.5"
1309
  source = { registry = "https://pypi.org/simple" }
1310
  dependencies = [
1311
  { name = "typing-extensions" },
1312
  ]
1313
- sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
1314
- wheels = [
1315
- { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 },
1316
- { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 },
1317
- { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 },
1318
- { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 },
1319
- { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 },
1320
- { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 },
1321
- { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 },
1322
- { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 },
1323
- { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 },
1324
- { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 },
1325
- { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 },
1326
- { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 },
1327
- { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 },
1328
- { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 },
1329
- { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 },
1330
- { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 },
1331
- { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 },
1332
- { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 },
1333
- { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 },
1334
- { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 },
1335
- { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 },
1336
- { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 },
1337
- { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 },
1338
- { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 },
1339
- { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 },
1340
- { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 },
1341
- { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 },
1342
- { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 },
1343
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
1344
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
1345
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
1346
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
1347
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
1348
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
1349
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
1350
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
1351
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
1352
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
1353
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
1354
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
1355
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
1356
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
1357
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
1358
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
1359
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
1360
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
1361
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
1362
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
1363
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
1364
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
1365
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
1366
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
1367
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
1368
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
1369
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
1370
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
1371
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
1372
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
1373
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
1374
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
1375
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
1376
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
1377
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
1378
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
1379
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
1380
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
1381
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
1382
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
1383
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
1384
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
1385
- { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 },
1386
- { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 },
1387
- { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 },
1388
- { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 },
1389
- { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 },
1390
- { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 },
1391
- { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 },
1392
- { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 },
1393
  ]
1394
 
1395
  [[package]]
 
758
  { name = "packaging" },
759
  { name = "pandas" },
760
  { name = "pillow" },
761
+ { name = "pydantic" },
762
  { name = "pyogrio" },
763
  { name = "pyparsing" },
764
  { name = "pyproj" },
 
793
  { name = "packaging", specifier = "==25.0" },
794
  { name = "pandas", specifier = "==2.3.3" },
795
  { name = "pillow", specifier = "==12.1.0" },
796
+ { name = "pydantic", specifier = "==2.11.1" },
797
  { name = "pyogrio", specifier = "==0.12.1" },
798
  { name = "pyparsing", specifier = "==3.3.1" },
799
  { name = "pyproj", specifier = "==3.7.2" },
 
1292
 
1293
  [[package]]
1294
  name = "pydantic"
1295
+ version = "2.11.1"
1296
  source = { registry = "https://pypi.org/simple" }
1297
  dependencies = [
1298
  { name = "annotated-types" },
 
1300
  { name = "typing-extensions" },
1301
  { name = "typing-inspection" },
1302
  ]
1303
+ sdist = { url = "https://files.pythonhosted.org/packages/93/a3/698b87a4d4d303d7c5f62ea5fbf7a79cab236ccfbd0a17847b7f77f8163e/pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968", size = 782817 }
1304
  wheels = [
1305
+ { url = "https://files.pythonhosted.org/packages/cc/12/f9221a949f2419e2e23847303c002476c26fbcfd62dc7f3d25d0bec5ca99/pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8", size = 442648 },
1306
  ]
1307
 
1308
  [[package]]
1309
  name = "pydantic-core"
1310
+ version = "2.33.0"
1311
  source = { registry = "https://pypi.org/simple" }
1312
  dependencies = [
1313
  { name = "typing-extensions" },
1314
  ]
1315
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/05/91ce14dfd5a3a99555fce436318cc0fd1f08c4daa32b3248ad63669ea8b4/pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3", size = 434080 }
1316
+ wheels = [
1317
+ { url = "https://files.pythonhosted.org/packages/f0/93/9e97af2619b4026596487a79133e425c7d3c374f0a7f100f3d76bcdf9c83/pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef", size = 2042784 },
1318
+ { url = "https://files.pythonhosted.org/packages/42/b4/0bba8412fd242729feeb80e7152e24f0e1a1c19f4121ca3d4a307f4e6222/pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a", size = 1858179 },
1319
+ { url = "https://files.pythonhosted.org/packages/69/1f/c1c40305d929bd08af863df64b0a26203b70b352a1962d86f3bcd52950fe/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b", size = 1909396 },
1320
+ { url = "https://files.pythonhosted.org/packages/0f/99/d2e727375c329c1e652b5d450fbb9d56e8c3933a397e4bd46e67c68c2cd5/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd", size = 1998264 },
1321
+ { url = "https://files.pythonhosted.org/packages/9c/2e/3119a33931278d96ecc2e9e1b9d50c240636cfeb0c49951746ae34e4de74/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b", size = 2140588 },
1322
+ { url = "https://files.pythonhosted.org/packages/35/bd/9267bd1ba55f17c80ef6cb7e07b3890b4acbe8eb6014f3102092d53d9300/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d", size = 2746296 },
1323
+ { url = "https://files.pythonhosted.org/packages/6f/ed/ef37de6478a412ee627cbebd73e7b72a680f45bfacce9ff1199de6e17e88/pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd", size = 2005555 },
1324
+ { url = "https://files.pythonhosted.org/packages/dd/84/72c8d1439585d8ee7bc35eb8f88a04a4d302ee4018871f1f85ae1b0c6625/pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453", size = 2124452 },
1325
+ { url = "https://files.pythonhosted.org/packages/a7/8f/cb13de30c6a3e303423751a529a3d1271c2effee4b98cf3e397a66ae8498/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b", size = 2087001 },
1326
+ { url = "https://files.pythonhosted.org/packages/83/d0/e93dc8884bf288a63fedeb8040ac8f29cb71ca52e755f48e5170bb63e55b/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915", size = 2261663 },
1327
+ { url = "https://files.pythonhosted.org/packages/4c/ba/4b7739c95efa0b542ee45fd872c8f6b1884ab808cf04ce7ac6621b6df76e/pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2", size = 2257786 },
1328
+ { url = "https://files.pythonhosted.org/packages/cc/98/73cbca1d2360c27752cfa2fcdcf14d96230e92d7d48ecd50499865c56bf7/pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86", size = 1925697 },
1329
+ { url = "https://files.pythonhosted.org/packages/9a/26/d85a40edeca5d8830ffc33667d6fef329fd0f4bc0c5181b8b0e206cfe488/pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b", size = 1949859 },
1330
+ { url = "https://files.pythonhosted.org/packages/7e/0b/5a381605f0b9870465b805f2c86c06b0a7c191668ebe4117777306c2c1e5/pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a", size = 1907978 },
1331
+ { url = "https://files.pythonhosted.org/packages/a9/c4/c9381323cbdc1bb26d352bc184422ce77c4bc2f2312b782761093a59fafc/pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43", size = 2025127 },
1332
+ { url = "https://files.pythonhosted.org/packages/6f/bd/af35278080716ecab8f57e84515c7dc535ed95d1c7f52c1c6f7b313a9dab/pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd", size = 1851687 },
1333
+ { url = "https://files.pythonhosted.org/packages/12/e4/a01461225809c3533c23bd1916b1e8c2e21727f0fea60ab1acbffc4e2fca/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6", size = 1892232 },
1334
+ { url = "https://files.pythonhosted.org/packages/51/17/3d53d62a328fb0a49911c2962036b9e7a4f781b7d15e9093c26299e5f76d/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6", size = 1977896 },
1335
+ { url = "https://files.pythonhosted.org/packages/30/98/01f9d86e02ec4a38f4b02086acf067f2c776b845d43f901bd1ee1c21bc4b/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4", size = 2127717 },
1336
+ { url = "https://files.pythonhosted.org/packages/3c/43/6f381575c61b7c58b0fd0b92134c5a1897deea4cdfc3d47567b3ff460a4e/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61", size = 2680287 },
1337
+ { url = "https://files.pythonhosted.org/packages/01/42/c0d10d1451d161a9a0da9bbef023b8005aa26e9993a8cc24dc9e3aa96c93/pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862", size = 2008276 },
1338
+ { url = "https://files.pythonhosted.org/packages/20/ca/e08df9dba546905c70bae44ced9f3bea25432e34448d95618d41968f40b7/pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a", size = 2115305 },
1339
+ { url = "https://files.pythonhosted.org/packages/03/1f/9b01d990730a98833113581a78e595fd40ed4c20f9693f5a658fb5f91eff/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099", size = 2068999 },
1340
+ { url = "https://files.pythonhosted.org/packages/20/18/fe752476a709191148e8b1e1139147841ea5d2b22adcde6ee6abb6c8e7cf/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6", size = 2241488 },
1341
+ { url = "https://files.pythonhosted.org/packages/81/22/14738ad0a0bf484b928c9e52004f5e0b81dd8dabbdf23b843717b37a71d1/pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3", size = 2248430 },
1342
+ { url = "https://files.pythonhosted.org/packages/e8/27/be7571e215ac8d321712f2433c445b03dbcd645366a18f67b334df8912bc/pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2", size = 1908353 },
1343
+ { url = "https://files.pythonhosted.org/packages/be/3a/be78f28732f93128bd0e3944bdd4b3970b389a1fbd44907c97291c8dcdec/pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48", size = 1955956 },
1344
+ { url = "https://files.pythonhosted.org/packages/21/26/b8911ac74faa994694b76ee6a22875cc7a4abea3c381fdba4edc6c6bef84/pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6", size = 1903259 },
1345
+ { url = "https://files.pythonhosted.org/packages/79/20/de2ad03ce8f5b3accf2196ea9b44f31b0cd16ac6e8cfc6b21976ed45ec35/pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555", size = 2032214 },
1346
+ { url = "https://files.pythonhosted.org/packages/f9/af/6817dfda9aac4958d8b516cbb94af507eb171c997ea66453d4d162ae8948/pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d", size = 1852338 },
1347
+ { url = "https://files.pythonhosted.org/packages/44/f3/49193a312d9c49314f2b953fb55740b7c530710977cabe7183b8ef111b7f/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365", size = 1896913 },
1348
+ { url = "https://files.pythonhosted.org/packages/06/e0/c746677825b2e29a2fa02122a8991c83cdd5b4c5f638f0664d4e35edd4b2/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da", size = 1986046 },
1349
+ { url = "https://files.pythonhosted.org/packages/11/ec/44914e7ff78cef16afb5e5273d480c136725acd73d894affdbe2a1bbaad5/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0", size = 2128097 },
1350
+ { url = "https://files.pythonhosted.org/packages/fe/f5/c6247d424d01f605ed2e3802f338691cae17137cee6484dce9f1ac0b872b/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885", size = 2681062 },
1351
+ { url = "https://files.pythonhosted.org/packages/f0/85/114a2113b126fdd7cf9a9443b1b1fe1b572e5bd259d50ba9d5d3e1927fa9/pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9", size = 2007487 },
1352
+ { url = "https://files.pythonhosted.org/packages/e6/40/3c05ed28d225c7a9acd2b34c5c8010c279683a870219b97e9f164a5a8af0/pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181", size = 2121382 },
1353
+ { url = "https://files.pythonhosted.org/packages/8a/22/e70c086f41eebd323e6baa92cc906c3f38ddce7486007eb2bdb3b11c8f64/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d", size = 2072473 },
1354
+ { url = "https://files.pythonhosted.org/packages/3e/84/d1614dedd8fe5114f6a0e348bcd1535f97d76c038d6102f271433cd1361d/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3", size = 2249468 },
1355
+ { url = "https://files.pythonhosted.org/packages/b0/c0/787061eef44135e00fddb4b56b387a06c303bfd3884a6df9bea5cb730230/pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b", size = 2254716 },
1356
+ { url = "https://files.pythonhosted.org/packages/ae/e2/27262eb04963201e89f9c280f1e10c493a7a37bc877e023f31aa72d2f911/pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585", size = 1916450 },
1357
+ { url = "https://files.pythonhosted.org/packages/13/8d/25ff96f1e89b19e0b70b3cd607c9ea7ca27e1dcb810a9cd4255ed6abf869/pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606", size = 1956092 },
1358
+ { url = "https://files.pythonhosted.org/packages/1b/64/66a2efeff657b04323ffcd7b898cb0354d36dae3a561049e092134a83e9c/pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225", size = 1908367 },
1359
+ { url = "https://files.pythonhosted.org/packages/52/54/295e38769133363d7ec4a5863a4d579f331728c71a6644ff1024ee529315/pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87", size = 1813331 },
1360
+ { url = "https://files.pythonhosted.org/packages/4c/9c/0c8ea02db8d682aa1ef48938abae833c1d69bdfa6e5ec13b21734b01ae70/pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b", size = 1986653 },
1361
+ { url = "https://files.pythonhosted.org/packages/8e/4f/3fb47d6cbc08c7e00f92300e64ba655428c05c56b8ab6723bd290bae6458/pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7", size = 1931234 },
1362
+ { url = "https://files.pythonhosted.org/packages/2b/b2/553e42762e7b08771fca41c0230c1ac276f9e79e78f57628e1b7d328551d/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c", size = 2041207 },
1363
+ { url = "https://files.pythonhosted.org/packages/85/81/a91a57bbf3efe53525ab75f65944b8950e6ef84fe3b9a26c1ec173363263/pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db", size = 1873736 },
1364
+ { url = "https://files.pythonhosted.org/packages/9c/d2/5ab52e9f551cdcbc1ee99a0b3ef595f56d031f66f88e5ca6726c49f9ce65/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b", size = 1903794 },
1365
+ { url = "https://files.pythonhosted.org/packages/2f/5f/a81742d3f3821b16f1265f057d6e0b68a3ab13a814fe4bffac536a1f26fd/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9", size = 2083457 },
1366
+ { url = "https://files.pythonhosted.org/packages/b5/2f/e872005bc0fc47f9c036b67b12349a8522d32e3bda928e82d676e2a594d1/pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c", size = 2119537 },
1367
+ { url = "https://files.pythonhosted.org/packages/d3/13/183f13ce647202eaf3dada9e42cdfc59cbb95faedd44d25f22b931115c7f/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976", size = 2080069 },
1368
+ { url = "https://files.pythonhosted.org/packages/23/8b/b6be91243da44a26558d9c3a9007043b3750334136c6550551e8092d6d96/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c", size = 2251618 },
1369
+ { url = "https://files.pythonhosted.org/packages/aa/c5/fbcf1977035b834f63eb542e74cd6c807177f383386175b468f0865bcac4/pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936", size = 2255374 },
1370
+ { url = "https://files.pythonhosted.org/packages/2f/f8/66f328e411f1c9574b13c2c28ab01f308b53688bbbe6ca8fb981e6cabc42/pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8", size = 2082099 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1371
  ]
1372
 
1373
  [[package]]