commit d6900dbf805359af3f281ff9a82ba11a097c4b5e Author: Hector van der Aa Date: Fri May 22 18:14:35 2026 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..31ff363 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,719 @@ +# dpg-map Architecture — Locked Rebuild + +This document defines the internal architecture for the rebuild. + +The central rule is: + +> Public API calls update logical state or enqueue commands. The GUI thread alone owns Dear PyGui items, draw commands, textures, handlers, and frame rendering. + +This prevents flicker, dragging failures, random recentering, and Dear PyGui thread-safety issues when overlays are updated from telemetry threads. + +## High-level architecture + +```text +User API + ↓ +Thread-safe command/logical-state layer + ↓ +Map registry and MapState + ↓ +Renderer frame pump on GUI thread + ↓ +Dear PyGui child_window + drawlist + textures +``` + +Detailed flow: + +```text +Telemetry thread / app thread / GUI callback + ↓ +public dpg_map function + ↓ +validate lightweight inputs + ↓ +acquire MapState.lock briefly + ↓ +update logical model and/or enqueue command + ↓ +mark specific dirty flags + ↓ +return immediately + ↓ +GUI frame callback drains commands + ↓ +renderer updates DPG draw layers/textures +``` + +## Critical invariants + +### Threading invariants + +1. Dear PyGui APIs are called only on the GUI thread. +2. Public API functions do not call Dear PyGui directly unless explicitly documented as GUI-thread-only. +3. Tile worker threads do network and disk work only. +4. Tile workers do not create or delete Dear PyGui textures. +5. Texture creation and deletion are queued to the renderer and executed on the GUI thread. +6. Runtime overlay updates are coalesced by overlay tag. +7. Locks are held briefly and never during network, disk, image decoding, or Dear PyGui calls. + +### View invariants + +1. `MapState.center` and `MapState.zoom` are changed only by view commands or interaction. +2. Overlay commands must never change center or zoom. +3. Tile loading must never change center or zoom. +4. Renderer redraw must render the current state; it must not infer a new center from overlays. +5. Provider switching may clamp zoom, but must not recenter unless the current center is invalid for the projection. + +### Rendering invariants + +1. Overlay redraw does not clear tile draw commands. +2. Tile redraw does not delete overlay logical state. +3. Provider switch clears only provider-specific tile resources. +4. Full redraw is reserved for initial render, size change, provider change, or recovery. +5. The renderer always reads a consistent snapshot of state for each frame. +6. DPG draw item tags are internal and not exposed as public overlay tags. + +### Sizing invariants + +1. The child window receives the requested Dear PyGui sizing intent. +2. The drawlist receives concrete measured dimensions. +3. Requested size and measured size are stored separately. +4. A measured size of zero while hidden must not permanently collapse the map. +5. The map should delay tile loading until a non-zero visible size is measured, unless explicitly configured otherwise. +6. Resize triggers tile and overlay redraw. + +## Package layout + +```text +src/dpg_map/ + __init__.py + api.py + widget.py + state.py + commands.py + renderer.py + draw_layers.py + interaction.py + overlays.py + providers.py + projection.py + tiles.py + cache.py + sizing.py + diagnostics.py + types.py + exceptions.py +examples/ + basic_map.py + sizing_window.py + sizing_child.py + sizing_table.py + hidden_tab.py + markers_live_thread.py + trajectory_live_thread.py + custom_provider.py + cache_stress.py +tests/ +``` + +## Module responsibilities + +### `__init__.py` + +Exports the public API only. + +No heavy initialization should happen here except registering the default provider. + +### `api.py` + +Contains thin public API wrappers, especially for runtime calls. + +Responsibilities: + +- resolve map/overlay tags +- validate simple arguments +- enqueue commands +- expose stable public functions + +The public API should call into `state.py`, `commands.py`, and `overlays.py`, but should not know renderer internals. + +### `commands.py` + +Defines commands that can be submitted from any thread. + +Suggested command model: + +```python +class CommandKind(Enum): + SET_VIEW = "set_view" + SET_PROVIDER = "set_provider" + ADD_OVERLAY = "add_overlay" + UPDATE_OVERLAY = "update_overlay" + DELETE_OVERLAY = "delete_overlay" + SET_LAYER_VISIBILITY = "set_layer_visibility" + CLEAR_LAYER = "clear_layer" + CLEAR_MAP = "clear_map" + CLEAR_MEMORY_CACHE = "clear_memory_cache" + CLEAR_DISK_CACHE = "clear_disk_cache" +``` + +Command dataclass: + +```python +@dataclass +class MapCommand: + kind: CommandKind + map_tag: str | int + payload: dict[str, Any] + created_at: float +``` + +Coalescing rules: + +- `UPDATE_OVERLAY` commands coalesce by `(map_tag, overlay_tag)`. +- repeated `SET_VIEW` commands may coalesce, keeping the newest. +- `ADD_OVERLAY`, `DELETE_OVERLAY`, `CLEAR_LAYER`, `CLEAR_MAP`, and `SET_PROVIDER` must preserve order. +- provider switches form a barrier; older tile results for the previous provider must be ignored. + +### `state.py` + +Owns configuration, registries, and `MapState`. + +Global config: + +```python +@dataclass +class DpgMapConfig: + user_agent: str | None = None + cache_dir: Path | None = None + default_provider: str | TileProvider = "osm" + memory_cache_max_tiles: int = 512 + disk_cache_max_bytes: int | None = 2_000_000_000 + prefetch_margin_tiles: int = 1 + tile_worker_count: int = 4 + overlay_update_policy: str = "coalesce" + debug: bool = False +``` + +MapState: + +```python +@dataclass +class MapState: + tag: str | int + + # DPG implementation tags, internal only + child_window_tag: str | int + drawlist_tag: str | int + texture_registry_tag: str | int + handler_registry_tag: str | int + + # sizing + requested_width: int + requested_height: int + requested_autosize_x: bool + requested_autosize_y: bool + measured_width: int = 0 + measured_height: int = 0 + last_nonzero_width: int = 0 + last_nonzero_height: int = 0 + is_visible: bool = False + + # view + center: LatLon = (0.0, 0.0) + zoom: int = 2 + min_zoom: int = 0 + max_zoom: int = 19 + provider: TileProvider = field(default_factory=get_default_provider) + + # models + overlays: dict[str | int, Overlay] = field(default_factory=dict) + layers: dict[str, LayerState] = field(default_factory=default_layers) + + # subsystems + command_queue: MapCommandQueue = field(default_factory=MapCommandQueue) + tile_manager: TileManager = field(default_factory=TileManager) + renderer: MapRenderer | None = None + interaction: InteractionState = field(default_factory=InteractionState) + + # concurrency + lock: RLock = field(default_factory=RLock) + + # redraw tracking + dirty: DirtyFlags = DirtyFlags.FULL + frame_scheduled: bool = False + generation: int = 0 +``` + +`generation` increments on provider switch, full clear, or other operations that invalidate pending tile results. + +### `types.py` + +Contains aliases and small dataclasses: + +```python +LatLon = tuple[float, float] +RGBA = tuple[int, int, int, int] +ScreenPoint = tuple[float, float] +``` + +### `exceptions.py` + +Contains public exceptions: + +```python +class DpgMapError(Exception): ... +class ProviderError(DpgMapError): ... +class UnknownProviderError(ProviderError): ... +class MapNotFoundError(DpgMapError): ... +class OverlayNotFoundError(DpgMapError): ... +class CoordinateError(DpgMapError): ... +class ThreadingError(DpgMapError): ... +class CacheError(DpgMapError): ... +``` + +### `providers.py` + +Defines the provider system. + +The default OpenStreetMap provider is registered at import time. + +Provider identity is part of `TileID`, so caches are provider-namespaced. + +### `projection.py` + +Pure Web Mercator math only. + +No Dear PyGui imports. + +No global state. + +Must include tests. + +### `overlays.py` + +Defines logical overlay dataclasses only. + +Overlay objects are not Dear PyGui draw items. + +```python +@dataclass +class Overlay: + tag: str | int + map_tag: str | int + layer: str + show: bool = True + user_data: Any = None + revision: int = 0 +``` + +`revision` increments when an overlay changes. The renderer can use this to decide whether overlay draw data needs rebuilding. + +Marker, Polyline, and Trajectory inherit from `Overlay`. + +Path overlays should copy incoming coordinate sequences into internal tuples/lists during command processing, not store references to mutable user lists. This prevents background buffers changing while the renderer reads them. + +### `widget.py` + +Creates the Dear PyGui structure. + +Internal structure: + +```text +child_window + drawlist +``` + +Rules: + +- child window receives the user-requested size. +- drawlist uses measured concrete content size. +- map is registered before entering the context body. +- current map stack is pushed for context-style overlay creation. +- frame callback is scheduled after creation to measure and render. +- resize/visibility handlers mark `SIZE_DIRTY`. + +The widget must not expose `child_window_tag` or `drawlist_tag`. + +### `sizing.py` + +Dedicated sizing helper module. + +Responsibilities: + +- measure child content region +- maintain last non-zero measured size +- decide whether tile loading can begin +- resize drawlist safely +- detect hidden-to-visible transitions +- avoid permanent collapse from zero-size measurements + +Suggested function: + +```python +def update_measured_size(state: MapState) -> bool: + """Return True if concrete drawlist size changed.""" +``` + +### `interaction.py` + +Owns panning and zooming. + +Responsibilities: + +- hit-test against concrete drawlist rectangle +- track active drag state +- update center during drag +- update zoom during wheel +- zoom around cursor where practical +- never allow overlay updates to reset interaction state + +Interaction should use the current measured drawlist rect, not only `dpg.is_item_hovered`. + +### `tiles.py` + +Owns tile identity, tile lifecycle, and visible tile calculation. + +TileID: + +```python +@dataclass(frozen=True) +class TileID: + provider_name: str + z: int + x: int + y: int +``` + +Tile states: + +```python +class TileStatus(Enum): + MISSING = "missing" + QUEUED = "queued" + LOADING = "loading" + READY = "ready" + FAILED = "failed" +``` + +Tile manager responsibilities: + +- compute visible tile IDs +- queue missing tiles +- avoid duplicate requests +- ignore stale tile results from old generations/providers +- manage memory cache metadata +- request texture creation on GUI thread +- request texture deletion on GUI thread +- protect currently visible tiles from eviction + +Tile workers: + +- check disk cache +- fetch network if needed +- decode image bytes to RGBA-compatible data if practical +- return loaded data to GUI queue +- never call DPG APIs + +### `cache.py` + +Owns persistent disk cache and memory cache policy. + +Disk path: + +```text +{cache_dir}/{provider_name}/{z}/{x}/{y}.{ext} +``` + +Metadata path: + +```text +{cache_dir}/{provider_name}/{z}/{x}/{y}.json +``` + +Metadata should support: + +```json +{ + "url": "...", + "etag": "...", + "last_modified": "...", + "expires": "...", + "downloaded_at": 0, + "last_accessed_at": 0, + "size_bytes": 0 +} +``` + +MVP disk cache behaviour: + +- file exists and is readable: use it +- file missing: fetch it +- update `last_accessed_at` +- prune by LRU when above `disk_cache_max_bytes` +- pruning must not delete files currently loading or currently visible + +HTTP-aware cache headers can be added after the stable MVP. + +### `renderer.py` + +The renderer is GUI-thread-only. + +Responsibilities: + +- drain command queue +- apply commands to logical state +- process tile worker results +- create/delete textures +- measure size +- compute visible tiles +- redraw dirty layers +- draw attribution/debug information + +The renderer must support separate draw layers. Dear PyGui drawlist does not provide retained sublayers in the same way as a scene graph, so implementation may use internal groups/tags or controlled delete/rebuild by tag prefix. The important rule is behavioural: + +- overlay update must not clear tile textures or tile logical state +- tile update must not modify overlay logical state +- full redraw must not reset view state + +### `draw_layers.py` + +Optional helper module for draw item bookkeeping. + +Suggested structure: + +```python +@dataclass +class DrawLayer: + name: str + item_tags: set[str | int] = field(default_factory=set) +``` + +Renderer can delete and rebuild only a selected layer. + +### `diagnostics.py` + +Provides debug state for examples and troubleshooting. + +```python +def get_map_debug_state(map_tag) -> dict[str, Any]: + ... +``` + +Should expose: + +- center +- zoom +- measured size +- requested size +- visible tile count +- queued/loading/failed tile counts +- memory cache count +- disk cache bytes +- overlay count +- dirty flags +- active drag state +- pending command count +- generation + +## Rendering frame lifecycle + +Each frame callback should roughly do: + +```text +for each map: + 1. update measured size + 2. drain pending public commands + 3. apply interaction input + 4. process tile results + 5. compute visible tiles if view/size/provider changed + 6. queue missing visible tiles + 7. create/delete textures requested by tile manager + 8. redraw required layers based on dirty flags + 9. clear dirty flags + 10. schedule next frame only if work remains or map needs continuous interaction polling +``` + +## Dirty flag model + +Use bit flags: + +```python +class DirtyFlags(IntFlag): + NONE = 0 + VIEW = auto() + TILES = auto() + OVERLAYS = auto() + SIZE = auto() + PROVIDER = auto() + ATTRIBUTION = auto() + DEBUG = auto() + FULL = VIEW | TILES | OVERLAYS | SIZE | PROVIDER | ATTRIBUTION | DEBUG +``` + +Expected usage: + +| Operation | Dirty flags | +|---|---| +| marker update | `OVERLAYS` | +| trajectory update | `OVERLAYS` | +| layer visibility | `OVERLAYS` | +| pan | `VIEW | TILES | OVERLAYS` | +| zoom | `VIEW | TILES | OVERLAYS` | +| resize | `SIZE | TILES | OVERLAYS` | +| provider switch | `PROVIDER | TILES | ATTRIBUTION` | +| cache tile ready | `TILES` | +| debug setting changed | `DEBUG` | + +## Command queue and coalescing + +The command queue must prevent flooding during telemetry. + +Recommended approach: + +```python +class MapCommandQueue: + ordered: deque[MapCommand] + overlay_updates: dict[tuple[map_tag, overlay_tag], MapCommand] + view_update: MapCommand | None + lock: Lock +``` + +When draining: + +1. preserve ordered structural commands +2. apply newest coalesced view command +3. apply newest overlay updates +4. mark dirty flags once + +Structural commands: + +- add overlay +- delete overlay +- clear layer +- clear map +- provider switch + +Coalesced commands: + +- update marker +- update polyline +- update trajectory +- set overlay show +- set center/zoom/view + +## Tile result generation safety + +Every tile request should include: + +```python +provider_name +generation +tile_id +``` + +When result returns: + +- if generation no longer matches map generation: discard result +- if provider no longer matches: discard result +- if tile no longer requested/visible/cacheable: optionally keep disk cache but do not create texture immediately + +This prevents stale workers from drawing old-provider tiles after a provider switch. + +## Persistent disk cache pruning + +Disk cache pruning should run: + +- at startup/open map +- after downloads push cache above the limit +- optionally manually via `clear_disk_cache` + +Pruning rules: + +1. calculate provider or global cache size +2. sort cached tile metadata by `last_accessed_at` +3. delete oldest until under target +4. never delete files currently visible/loading +5. tolerate missing/corrupt metadata + +## Sizing strategy + +The map widget uses two sizes: + +```python +requested_width / requested_height +measured_width / measured_height +``` + +The child window is created with requested values: + +```python +dpg.add_child_window(width=requested_width, height=requested_height, ...) +``` + +The drawlist is resized to measured content size: + +```python +content_width = dpg.get_item_width(child_window_tag) +content_height = dpg.get_item_height(child_window_tag) +dpg.configure_item(drawlist_tag, width=content_width, height=content_height) +``` + +If measured size is zero: + +- keep last non-zero size +- do not permanently configure drawlist to zero +- if no last non-zero size exists, render placeholder only and delay tile loading + +## Initial setup instructions + +These replace the old Step 0. + +```bash +mkdir dpg-map +cd dpg-map +git init +uv init --package dpg-map +uv add dearpygui pillow platformdirs requests +uv add --dev pytest ruff pyright +``` + +Create: + +```text +src/dpg_map/ +examples/ +tests/ +FEATURES.md +ARCHITECTURE.md +STEPS.md +AGENTS.md +README.md +``` + +Use `AGENTS.md` as the rolling implementation log. After every step: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +git status +git add . +git commit -m "step N: short description" +``` + +## Development examples required + +The rebuild must include examples specifically designed to reproduce the first build's failure modes: + +```text +examples/basic_map.py +examples/sizing_window.py +examples/sizing_child.py +examples/sizing_table.py +examples/hidden_tab.py +examples/markers_live_thread.py +examples/trajectory_live_thread.py +examples/custom_provider.py +examples/cache_stress.py +``` diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..528b7bb --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,554 @@ +# dpg-map Feature Specification — Locked API Rebuild + +This document defines the public behaviour and API contract for the next rebuild of `dpg-map`. + +The first build reached overlay/layer functionality, but suffered from rendering instability, flickering, bad drag behaviour while overlays were updating, occasional unwanted recentering, cache limitations, and sizing mismatches between the child container and the drawlist. + +The rebuild must treat the public API as a stable contract from the beginning. + +## Project summary + +`dpg-map` is a Python dependency providing a Dear PyGui map widget for raster XYZ map tiles and geographic overlays. + +It should be usable as: + +```python +import dpg_map as dpgm +``` + +The widget should feel natural in Dear PyGui code: + +```python +with dpgm.map_widget(tag="map", center=(47.9029, 1.9093), zoom=15, width=-1, height=-1): + dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093) + dpgm.add_trajectory("lap", lats=[], lons=[]) +``` + +Runtime updates from telemetry or worker threads must be safe: + +```python +dpgm.update_marker("vehicle", lat=current_lat, lon=current_lon) +dpgm.update_trajectory("lap", lats=lat_buffer, lons=lon_buffer) +``` + +Those calls must not directly call Dear PyGui APIs, block the caller, reset the map view, flicker the map, or interfere with dragging/zooming. + +## Core requirements + +### 1. Stable public API + +The public API is considered locked once implemented. Internals may change, but user code should not need to change. + +Required public areas: + +- global configuration +- tile providers +- map widget creation +- map view state +- overlays +- layers +- cache configuration +- diagnostics/debugging + +### 2. Thread-safe runtime updates + +Any public runtime function may be called from a non-GUI thread unless explicitly documented otherwise. + +Examples: + +```python +dpgm.update_marker(...) +dpgm.update_polyline(...) +dpgm.update_trajectory(...) +dpgm.set_overlay_show(...) +dpgm.delete_overlay(...) +dpgm.set_center(...) +dpgm.set_zoom(...) +dpgm.set_view(...) +dpgm.set_provider(...) +dpgm.clear_layer(...) +``` + +Rules: + +- Public runtime functions must be non-blocking or near non-blocking. +- They must never call Dear PyGui draw, texture, item, handler, or viewport APIs directly. +- They must update a thread-safe logical state model and/or enqueue commands. +- The GUI thread drains the queue and performs Dear PyGui work. +- Overlay updates must not mutate map center, zoom, pan state, active drag state, or tile provider unless the function is explicitly a view/provider function. +- Overlay updates must be coalesced, so a telemetry thread can update at high rate without flooding the GUI thread. + +### 3. Interaction must remain stable during overlay updates + +While markers, polylines, or trajectories are being added/updated/deleted: + +- the user must still be able to drag the map +- the user must still be able to zoom +- the map must not recenter unexpectedly +- the map must not flash through an empty state +- tile draw commands should not be destroyed just because an overlay changed +- overlay draw refreshes should be isolated from tile draw refreshes where possible + +### 4. Persistent tile caching + +The cache must have two independent limits: + +```python +memory_cache_max_tiles: int = 512 +disk_cache_max_bytes: int | None = None +``` + +Memory cache: + +- runtime only +- LRU or approximate LRU +- stores decoded tile data and/or active texture references +- protects currently visible tiles from eviction +- evicts Dear PyGui textures only on the GUI thread + +Disk cache: + +- persistent across application restarts +- provider-namespaced +- configurable path +- configurable size limit +- pruned in the background or during startup +- avoids repeatedly fetching regularly used tiles + +Default disk cache path should use `platformdirs` unless overridden. + +### 5. Sizing must be robust + +The Dear PyGui child container and internal drawlist must stay in sync. + +Requirements: + +- `width=0` and `height=0` preserve Dear PyGui default behaviour. +- `width=-1` fills available width. +- `height=-1` fills available height where Dear PyGui allows it. +- fixed positive dimensions are respected. +- `autosize_x` and `autosize_y` are supported where practical. +- the child window stores the requested sizing intent. +- the drawlist always uses measured concrete pixel dimensions. +- the drawlist must resize when the child content region changes. +- the map must work inside windows, groups, child windows, tabs, tables, and hidden/show-later layouts. +- the renderer must not get stuck at the first measured size. + +### 6. Interchangeable TileProvider system + +OpenStreetMap is the default provider, but users must be able to use custom XYZ providers. + +```python +provider = dpgm.TileProvider( + name="custom", + url_template="https://example.com/{z}/{x}/{y}.png", + attribution="Tiles © Example", +) +dpgm.register_provider(provider) +``` + +Provider switching should preserve overlays and the current center where possible. + +### 7. uv-first package management + +The project must be managed with `uv`. + +Users should be able to install locally with: + +```bash +uv add -e ../dpg-map +``` + +The package must support normal `uv add`, `uv sync`, `uv run pytest`, and editable local development. + +## Non-goals for the first rebuilt version + +Do not implement these in the first rebuild: + +- vector map rendering +- Mapbox GL rendering +- geocoding/search +- routing +- offline planet files +- arbitrary projections +- map rotation/pitch +- complex hit testing for lines/polygons +- GPU-accelerated custom renderers +- tile blending/fade animations + +## Public API + +### Global configuration + +```python +dpgm.configure( + *, + user_agent: str | None = None, + cache_dir: str | Path | None = None, + default_provider: str | dpgm.TileProvider = "osm", + memory_cache_max_tiles: int = 512, + disk_cache_max_bytes: int | None = 2_000_000_000, + prefetch_margin_tiles: int = 1, + tile_worker_count: int = 4, + overlay_update_policy: str = "coalesce", + debug: bool = False, +) -> None +``` + +Notes: + +- `disk_cache_max_bytes=None` means unlimited disk cache. +- `overlay_update_policy="coalesce"` means only the newest update for each overlay is rendered. +- OpenStreetMap usage should require or strongly warn about a valid application-specific `user_agent`. + +### TileProvider + +```python +@dataclass(frozen=True) +class TileProvider: + name: str + url_template: str + min_zoom: int = 0 + max_zoom: int = 19 + tile_size: int = 256 + attribution: str = "" + headers: dict[str, str] = field(default_factory=dict) + subdomains: tuple[str, ...] = () + retina: bool = False + file_extension: str | None = None +``` + +Provider helpers: + +```python +dpgm.register_provider(provider: dpgm.TileProvider) -> None +dpgm.unregister_provider(name: str) -> None +dpgm.get_provider(name: str) -> dpgm.TileProvider +dpgm.list_providers() -> list[str] +``` + +Default provider: + +```python +OSM = TileProvider( + name="osm", + url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png", + min_zoom=0, + max_zoom=19, + tile_size=256, + attribution="© OpenStreetMap contributors", +) +``` + +Provider URL template rules: + +- `{z}`, `{x}`, and `{y}` are required. +- `{s}` is optional for subdomains. +- `{r}` is optional for retina suffixes. +- invalid templates must raise a clear provider exception. + +### Map widget + +```python +with dpgm.map_widget( + tag: str | int | None = None, + center: tuple[float, float] = (0.0, 0.0), + zoom: int = 2, + min_zoom: int | None = None, + max_zoom: int | None = None, + width: int = 0, + height: int = 0, + autosize_x: bool = False, + autosize_y: bool = False, + border: bool = True, + provider: str | dpgm.TileProvider | None = None, + cache_dir: str | Path | None = None, + user_agent: str | None = None, + no_scrollbar: bool = True, + delay_tile_load_until_visible: bool = True, +) as map_tag: + ... +``` + +The returned `map_tag` is the logical map tag, not a Dear PyGui item tag. + +### View state API + +```python +dpgm.set_center(map_tag, lat: float, lon: float) -> None +dpgm.get_center(map_tag) -> tuple[float, float] + +dpgm.set_zoom(map_tag, zoom: int) -> None +dpgm.get_zoom(map_tag) -> int + +dpgm.set_view( + map_tag, + *, + center: tuple[float, float] | None = None, + zoom: int | None = None, +) -> None + +dpgm.fit_bounds( + map_tag, + min_lat: float, + min_lon: float, + max_lat: float, + max_lon: float, + padding: int = 32, +) -> None + +dpgm.screen_to_latlon(map_tag, x: float, y: float) -> tuple[float, float] +dpgm.latlon_to_screen(map_tag, lat: float, lon: float) -> tuple[float, float] +``` + +View state commands may be called from other threads, but they are queued and applied on the GUI thread. `get_*` functions return the latest committed logical state. + +### Overlay creation API + +All overlay creation functions can be used inside a map context or at runtime with `map_tag`. + +#### Marker + +```python +dpgm.add_marker( + tag: str | int, + lat: float, + lon: float, + *, + label: str | None = None, + color: tuple[int, int, int, int] = (255, 80, 80, 255), + radius: float = 5.0, + layer: str = "markers", + show: bool = True, + show_label: bool = False, + user_data: Any = None, + callback: Callable | None = None, + map_tag: str | int | None = None, +) -> str | int +``` + +#### Polyline + +```python +dpgm.add_polyline( + tag: str | int, + points: Sequence[tuple[float, float]] | None = None, + *, + lats: Sequence[float] | None = None, + lons: Sequence[float] | None = None, + color: tuple[int, int, int, int] = (80, 180, 255, 255), + thickness: float = 2.0, + layer: str = "lines", + show: bool = True, + closed: bool = False, + simplify: bool = True, + user_data: Any = None, + map_tag: str | int | None = None, +) -> str | int +``` + +#### Trajectory + +```python +dpgm.add_trajectory( + tag: str | int, + lats: Sequence[float], + lons: Sequence[float], + *, + timestamps: Sequence[float] | None = None, + color: tuple[int, int, int, int] = (255, 180, 60, 255), + thickness: float = 2.0, + show_points: bool = False, + point_stride: int = 1, + layer: str = "trajectories", + show: bool = True, + user_data: Any = None, + map_tag: str | int | None = None, +) -> str | int +``` + +### Overlay update API + +Overlay update APIs must be safe from background threads and must not directly touch Dear PyGui. + +```python +dpgm.update_marker( + tag: str | int, + *, + lat: float | None = None, + lon: float | None = None, + label: str | None = None, + show: bool | None = None, + color: tuple[int, int, int, int] | None = None, + radius: float | None = None, +) -> None +``` + +```python +dpgm.update_polyline( + tag: str | int, + points: Sequence[tuple[float, float]] | None = None, + *, + lats: Sequence[float] | None = None, + lons: Sequence[float] | None = None, + show: bool | None = None, + color: tuple[int, int, int, int] | None = None, + thickness: float | None = None, +) -> None +``` + +```python +dpgm.update_trajectory( + tag: str | int, + *, + lats: Sequence[float] | None = None, + lons: Sequence[float] | None = None, + timestamps: Sequence[float] | None = None, + show: bool | None = None, + color: tuple[int, int, int, int] | None = None, + thickness: float | None = None, +) -> None +``` + +Convenience aliases: + +```python +dpgm.set_marker_position(tag, lat, lon) -> None +dpgm.set_marker_label(tag, label) -> None +dpgm.set_polyline_points(tag, points=None, *, lats=None, lons=None) -> None +dpgm.set_overlay_show(tag, show: bool) -> None +dpgm.delete_overlay(tag) -> None +``` + +### Layer API + +```python +dpgm.add_layer(map_tag, name: str, z_index: int) -> None +dpgm.show_layer(map_tag, name: str) -> None +dpgm.hide_layer(map_tag, name: str) -> None +dpgm.clear_layer(map_tag, name: str) -> None +``` + +Layer operations are queued and applied by the GUI thread. + +### Provider switching + +```python +dpgm.set_provider(map_tag, provider: str | dpgm.TileProvider) -> None +``` + +Provider switching must: + +- keep overlays +- keep center where possible +- clamp zoom to provider limits +- clear or detach old tile draw layer only +- update attribution +- not rebuild overlays unnecessarily +- not call Dear PyGui from the caller thread + +### Cache control API + +```python +dpgm.clear_memory_cache(map_tag: str | int | None = None) -> None +dpgm.clear_disk_cache(provider: str | None = None) -> None +dpgm.get_cache_stats(map_tag: str | int | None = None) -> dpgm.CacheStats +``` + +`CacheStats` should include at least: + +```python +@dataclass(frozen=True) +class CacheStats: + memory_tiles: int + memory_limit_tiles: int + disk_bytes: int + disk_limit_bytes: int | None + queued_tiles: int + loading_tiles: int + failed_tiles: int +``` + +### Diagnostics + +```python +dpgm.get_map_debug_state(map_tag) -> dict[str, Any] +``` + +Used to debug: + +- measured child size +- drawlist size +- center/zoom +- visible tile count +- overlay count +- pending command count +- cache stats +- whether the map is currently considered visible/hovered/dragging + +## Rendering stability requirements + +### Dirty flags + +The implementation must separate dirty reasons: + +```python +VIEW_DIRTY +TILES_DIRTY +OVERLAYS_DIRTY +SIZE_DIRTY +PROVIDER_DIRTY +FULL_DIRTY +``` + +Overlay updates should set `OVERLAYS_DIRTY`, not `FULL_DIRTY`. + +Panning/zooming should set `VIEW_DIRTY` and likely `TILES_DIRTY | OVERLAYS_DIRTY`. + +Resizing should set `SIZE_DIRTY | TILES_DIRTY | OVERLAYS_DIRTY`. + +Provider switching should set `PROVIDER_DIRTY | TILES_DIRTY`, but should not delete overlay logical state. + +### Draw layers + +Internally, the renderer should use separate draw layers/groups where possible: + +```text +background layer +tile layer +overlay layer +attribution/debug layer +``` + +Overlay redraws should not wipe tile draw commands unless the view, size, or provider changed. + +### View ownership + +Only these may intentionally change map center/zoom: + +- user pan/zoom interaction +- `set_center` +- `set_zoom` +- `set_view` +- `fit_bounds` +- provider switch only if zoom must be clamped + +Overlay creation/update/delete must never recenter the map. + +## Acceptance tests for the rebuild + +The following manual and automated examples are required before treating the rebuild as usable: + +1. Basic map shows OSM tiles. +2. Map fills a resizable Dear PyGui window. +3. Map inside a child window fills its container. +4. Map inside a table cell fills its cell. +5. Map inside a hidden tab starts loading when shown. +6. Dragging works while a marker is updated at 20–60 Hz from another thread. +7. Dragging works while a trajectory is updated at 10–30 Hz from another thread. +8. No unexpected center change occurs during overlay updates. +9. Returning to a recently viewed area uses memory cache. +10. Restarting the app and viewing the same area uses disk cache. +11. Disk cache prunes to the configured limit. +12. Provider switch keeps overlays. +13. `uv add -e ../dpg-map` works from another project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/STEPS.md b/STEPS.md new file mode 100644 index 0000000..890ecb7 --- /dev/null +++ b/STEPS.md @@ -0,0 +1,764 @@ +# dpg-map Rebuild Steps + +This file replaces the old long step list. + +There is no Step 0. Initial setup is listed separately, then implementation starts at Step 1. + +## Workflow rules + +1. Use `uv` for all Python package and dependency management. +2. Always read `FEATURES.md`, `ARCHITECTURE.md`, and `AGENTS.md` before making code changes. +3. Keep `AGENTS.md` as a rolling log of what has been done, what is broken, and what comes next. +4. Update `README.md` whenever public behaviour or examples change. +5. After every step: + - update `AGENTS.md` + - run relevant checks + - commit to git +6. Do not casually change public API once introduced. +7. Public runtime APIs must be safe to call from background threads unless explicitly documented otherwise. +8. Dear PyGui calls must happen on the GUI thread only. +9. Prefer stable, boring implementation over clever rendering tricks. + +## Initial setup instructions + +Run these before Step 1 when starting from a clean repository: + +```bash +mkdir dpg-map +cd dpg-map +git init +uv init --package dpg-map +uv add dearpygui pillow platformdirs requests +uv add --dev pytest ruff pyright +``` + +Create this structure: + +```text +src/dpg_map/ + __init__.py + api.py + widget.py + state.py + commands.py + renderer.py + draw_layers.py + interaction.py + overlays.py + providers.py + projection.py + tiles.py + cache.py + sizing.py + diagnostics.py + types.py + exceptions.py +examples/ +tests/ +FEATURES.md +ARCHITECTURE.md +STEPS.md +AGENTS.md +README.md +``` + +Recommended `pyproject.toml` additions: + +```toml +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM"] + +[tool.pyright] +typeCheckingMode = "basic" +``` + +Create `AGENTS.md` with: + +```markdown +# AGENTS.md + +## Current status + +Rebuild initialized. + +## Completed steps + +None yet. + +## Current step + +Step 1 — Public API contract and pure core. + +## Design decisions + +- Package is managed with uv. +- Public import is `import dpg_map as dpgm`. +- Dear PyGui calls are GUI-thread-only. +- Runtime public calls enqueue commands or update logical state. +- Overlay updates must not reset center/zoom. +- The widget uses child_window + measured-size drawlist. +- Tiles use a memory cache and persistent disk cache. +- Tile providers are interchangeable. + +## Known issues + +None yet. + +## Commands used + +None yet. + +## Next action + +Implement Step 1. +``` + +Initial commit: + +```bash +uv run python -c "import dpg_map; print(dpg_map.__name__)" +uv run ruff check . +uv run ruff format . +git add . +git commit -m "initial uv project setup" +``` + +## Step 1 — Public API contract and pure core + +Goal: lock the public API surface and implement pure non-DPG components first. + +Tasks: + +1. Define public exports in `__init__.py`. + +Required API names: + +```python +configure + +TileProvider +register_provider +unregister_provider +get_provider +list_providers + +map_widget + +set_center +get_center +set_zoom +get_zoom +set_view +fit_bounds +screen_to_latlon +latlon_to_screen + +add_marker +add_polyline +add_trajectory +update_marker +update_polyline +update_trajectory +set_marker_position +set_marker_label +set_polyline_points +set_overlay_show +delete_overlay + +add_layer +show_layer +hide_layer +clear_layer +clear_map + +set_provider + +clear_memory_cache +clear_disk_cache +get_cache_stats +get_map_debug_state +``` + +2. Add exceptions in `exceptions.py`. + +3. Add common types in `types.py`. + +4. Implement `TileProvider` and provider registry in `providers.py`. + +5. Implement Web Mercator projection in `projection.py`. + +6. Implement initial cache dataclasses and `CacheStats`. + +7. Stub GUI-dependent public functions so imports succeed. + +8. Add tests for: + - package imports + - provider registration + - provider URL building + - invalid provider templates + - projection roundtrips + - cache stats dataclass construction + +Checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- public API imports cleanly +- provider system works +- projection tests pass +- no Dear PyGui context is required for tests +- API names are fixed before renderer work begins + +Commit: + +```bash +git add . +git commit -m "step 1: lock public api and pure core" +``` + +## Step 2 — Thread-safe state, commands, overlays, and cache model + +Goal: build the logical state model before rendering anything. + +Tasks: + +1. Implement `DpgMapConfig` and `configure(...)`. + +2. Implement `MapState`. + +3. Implement global map registry and current map context stack. + +4. Implement `DirtyFlags`. + +5. Implement `MapCommand`, `CommandKind`, and `MapCommandQueue`. + +6. Implement coalescing rules: + - latest overlay update per overlay wins + - latest view update wins + - structural commands preserve order + +7. Implement logical overlay dataclasses: + - `Overlay` + - `MarkerOverlay` + - `PolylineOverlay` + - `TrajectoryOverlay` + - `LayerState` + +8. Implement public overlay creation/update functions so they: + - resolve map context or `map_tag` + - validate inputs + - copy coordinate sequences + - enqueue commands or mutate logical model under lock + - never call Dear PyGui + +9. Implement cache model: + - memory cache metadata + - disk path generation + - disk metadata read/write + - disk size scanning + - prune planning without deleting active files yet + +10. Add tests for: + - command coalescing + - overlay update does not alter center/zoom + - trajectory input lists are copied + - mismatched lat/lon lengths raise + - layer state + - disk cache path generation + - disk cache prune ordering + +Checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- runtime overlay update functions are thread-safe at the logical level +- no public overlay function calls Dear PyGui +- center/zoom cannot be changed by overlay commands +- cache has separate memory and disk configuration +- persistent disk cache layout is decided + +Commit: + +```bash +git add . +git commit -m "step 2: add thread safe state commands and cache model" +``` + +## Step 3 — Widget shell, sizing system, and GUI-thread frame pump + +Goal: create a stable Dear PyGui widget shell before loading real tiles. + +Tasks: + +1. Implement `map_widget(...)`. + +2. Internally create: + +```text +child_window + drawlist +``` + +3. Child window receives requested sizing values. + +4. Drawlist receives concrete measured size only. + +5. Implement `sizing.py`: + - measure child content region + - track last non-zero size + - resize drawlist + - detect hidden/show-later transitions + - avoid permanent zero-size collapse + +6. Implement the renderer frame pump: + - schedules frame callbacks + - drains commands on GUI thread + - updates size + - draws background and attribution placeholder + - exposes debug state + +7. Implement map interaction hit-test rectangle calculation, but not full panning yet. + +8. Add examples: + - `examples/basic_map.py` + - `examples/sizing_window.py` + - `examples/sizing_child.py` + - `examples/sizing_table.py` + - `examples/hidden_tab.py` + +9. Add tests where possible without opening a GUI: + - sizing state transitions + - zero-size does not overwrite last non-zero size + - dirty flags from resize + - command drain ordering + +Manual checks: + +```bash +uv run python examples/basic_map.py +uv run python examples/sizing_window.py +uv run python examples/sizing_child.py +uv run python examples/sizing_table.py +uv run python examples/hidden_tab.py +``` + +Automated checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- widget appears +- map area fills its child container correctly +- drawlist size matches measured child content size +- hidden tab example loads/render placeholder after becoming visible +- no map gets stuck at zero size +- public API remains unchanged + +Commit: + +```bash +git add . +git commit -m "step 3: add widget shell sizing and frame pump" +``` + +## Step 4 — Tile manager, persistent cache, and asynchronous loading + +Goal: implement stable tile loading and both cache limits before advanced overlays. + +Tasks: + +1. Implement `TileID`, `TileStatus`, `Tile`, and `TileManager`. + +2. Implement visible tile calculation. + +3. Implement persistent disk cache: + - provider-namespaced paths + - metadata files + - `last_accessed_at` + - disk size scanning + - LRU pruning to `disk_cache_max_bytes` + +4. Implement memory cache: + - LRU or approximate LRU + - `memory_cache_max_tiles` + - visible-tile protection + - deferred GUI-thread texture deletion + +5. Implement worker thread/tile queue: + - read disk cache + - fetch HTTP if missing + - decode tile image + - return tile result with map generation and provider name + - never call Dear PyGui + +6. Renderer GUI thread: + - processes tile results + - ignores stale generation/provider results + - creates textures + - deletes evicted textures + - draws tile layer + +7. Ensure OpenStreetMap provider uses configured User-Agent or warns/raises clearly. + +8. Add `examples/cache_stress.py`. + +Manual checks: + +```bash +uv run python examples/basic_map.py +uv run python examples/cache_stress.py +``` + +Automated checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- OSM tiles display +- GUI does not freeze while tiles load +- only visible tiles plus margin are requested +- returning to recently viewed area uses memory cache +- restarting app uses disk cache +- disk cache prunes to configured limit +- stale tile results are ignored after provider/generation change +- texture deletion happens on GUI thread + +Commit: + +```bash +git add . +git commit -m "step 4: add async tiles and persistent cache" +``` + +## Step 5 — Interaction: pan, zoom, and view commands + +Goal: make interaction stable before adding live overlays. + +Tasks: + +1. Implement left mouse drag panning. + +2. Implement mouse wheel zoom. + +3. Zoom around cursor where practical. + +4. Implement programmatic view commands: + - `set_center` + - `set_zoom` + - `set_view` + - `fit_bounds` + - `screen_to_latlon` + - `latlon_to_screen` + +5. Interaction must: + - use concrete map rectangle + - work in nested containers + - not depend only on child-window hover state + - not reset when overlay commands are queued + - not recenter from tile results + +6. Add debug fields for: + - active drag + - last mouse position + - current center + - current zoom + - dirty flags + +Manual checks: + +```bash +uv run python examples/basic_map.py +uv run python examples/sizing_child.py +uv run python examples/sizing_table.py +``` + +Automated checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- dragging pans smoothly +- mouse wheel zooms +- tile grid remains aligned +- map works in nested containers +- view commands work +- no unexpected recenter occurs from tile loading + +Commit: + +```bash +git add . +git commit -m "step 5: add stable pan zoom and view commands" +``` + +## Step 6 — Overlay rendering and runtime update stress tests + +Goal: implement overlays in a way that cannot break interaction. + +Tasks: + +1. Implement marker rendering. + +2. Implement polyline rendering. + +3. Implement trajectory rendering. + +4. Implement overlay layer rendering separate from tile layer. + +5. Overlay updates must set only `OVERLAYS` dirty unless a structural map operation requires more. + +6. Overlay draw rebuild must not: + - clear tile textures + - clear tile draw state + - reset center + - reset zoom + - reset active drag state + - rebuild the whole map unnecessarily + +7. Implement: + - `add_marker` + - `add_polyline` + - `add_trajectory` + - `update_marker` + - `update_polyline` + - `update_trajectory` + - `set_marker_position` + - `set_marker_label` + - `set_polyline_points` + - `set_overlay_show` + - `delete_overlay` + +8. Add examples: + - `examples/markers_live_thread.py` + - `examples/trajectory_live_thread.py` + +The live examples must update overlays from a background thread while the user drags and zooms. + +Manual stress checks: + +```bash +uv run python examples/markers_live_thread.py +uv run python examples/trajectory_live_thread.py +``` + +While examples run: + +- drag continuously for at least 30 seconds +- zoom in and out +- verify no flicker through blank map +- verify no random recentering +- verify overlay remains geographically attached +- verify update thread does not block badly + +Automated checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- marker updates at 20–60 Hz do not break dragging +- trajectory updates at 10–30 Hz do not break dragging +- overlay updates are coalesced +- center/zoom remain stable unless explicitly changed +- overlay update APIs are safe from background threads +- tile layer is not destroyed by overlay updates + +Commit: + +```bash +git add . +git commit -m "step 6: add stable overlays and live update stress tests" +``` + +## Step 7 — Layers, provider switching, and clearing APIs + +Goal: add structural map operations after rendering stability is proven. + +Tasks: + +1. Implement layer operations: + - `add_layer` + - `show_layer` + - `hide_layer` + - `clear_layer` + - `clear_map` + +2. Implement provider switching: + - `set_provider` + +3. Provider switch must: + - keep overlays + - keep center + - clamp zoom if needed + - increment map generation + - clear old provider tile draw state + - ignore stale tile results + - redraw attribution + - not perform Dear PyGui work from caller thread + +4. Implement cache control: + - `clear_memory_cache` + - `clear_disk_cache` + - `get_cache_stats` + +5. Add examples: + - `examples/custom_provider.py` + - extend `examples/cache_stress.py` + +Manual checks: + +```bash +uv run python examples/custom_provider.py +uv run python examples/cache_stress.py +``` + +Automated checks: + +```bash +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- layers can be shown/hidden/cleared +- provider switch keeps overlays +- provider switch does not render stale old-provider tiles +- cache clear functions do not call Dear PyGui from non-GUI threads +- memory/disk cache stats are usable + +Commit: + +```bash +git add . +git commit -m "step 7: add layers provider switching and cache controls" +``` + +## Step 8 — Documentation, hardening, and internal release + +Goal: make the package usable as a dependency in DataFlux or another project. + +Tasks: + +1. Update `README.md` with: + - uv install + - local editable dependency + - basic map + - sizing examples + - live marker update + - live trajectory update + - custom provider + - memory/disk cache config + - OpenStreetMap attribution/User-Agent note + - thread-safety contract + +2. Add docstrings for all public functions. + +3. Review public API exports. + +4. Add tests for failure cases: + - unknown map + - unknown overlay + - unknown provider + - invalid coordinates + - mismatched lat/lon lengths + - empty trajectory + - clear deleted overlay + - provider switch while tiles loading + - overlay update while dragging model state + +5. Run all examples manually. + +6. Test local editable install from another project: + +```bash +cd ../some-test-project +uv add -e ../dpg-map +uv run python -c "import dpg_map as dpgm; print(dpgm.list_providers())" +``` + +7. Bump version. + +Suggested initial rebuilt beta: + +```text +0.3.0b1 +``` + +8. Tag release: + +```bash +git tag v0.3.0b1 +``` + +Final checks: + +```bash +uv sync +uv run pytest +uv run ruff check . +uv run ruff format --check . +uv run pyright +``` + +Acceptance criteria: + +- all examples run +- local editable dependency works +- live overlay stress examples are stable +- cache survives restart +- disk cache limit works +- sizing examples work +- README accurately documents thread-safety and cache behaviour +- AGENTS.md accurately describes status + +Commit: + +```bash +git add . +git commit -m "step 8: harden docs and prepare rebuilt beta" +git tag v0.3.0b1 +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..05954ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "dpg-map" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "Hector van der Aa", email = "hector@h3cx.dev" } +] +requires-python = ">=3.14" +dependencies = [ + "dearpygui>=2.3.1", + "pillow>=12.2.0", + "platformdirs>=4.9.6", + "requests>=2.34.2", +] + +[project.scripts] +dpg-map = "dpg_map:main" + +[build-system] +requires = ["uv_build>=0.11.14,<0.12.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "pyright>=1.1.409", + "pytest>=9.0.3", + "ruff>=0.15.14", +] diff --git a/src/dpg_map/__init__.py b/src/dpg_map/__init__.py new file mode 100644 index 0000000..2ff2eb0 --- /dev/null +++ b/src/dpg_map/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Hello from dpg-map!") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..60f75dc --- /dev/null +++ b/uv.lock @@ -0,0 +1,289 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dearpygui" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/08/68ce3ba941cf09bcf288f2603d7178c936d473172fd545e0665b885cb840/dearpygui-2.3.1-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:e2cbabd8d383308ea3520360e100d1e95c9a957cd26247431cd4920b23c3b56e", size = 2003034, upload-time = "2026-05-01T22:46:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3f/ac003b6d636c9f5aae910a62d00b6c119403212cf2d7772d7c59ca8ef6dd/dearpygui-2.3.1-cp314-cp314-manylinux1_x86_64.whl", hash = "sha256:953442c7272e57f3686e393edc5cf6ce73cc2f47142739735dfabed403e33770", size = 2678545, upload-time = "2026-05-01T22:46:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6b/657a2f1e2f604f356c39ed5b870aaed5d07fdeac3139fae2a455473b64ca/dearpygui-2.3.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:43a561b5dc589944a3a2b469e5e68f1ab35ae38b3dcdf1f813ed9d6e024153f8", size = 2501699, upload-time = "2026-05-01T22:46:21.497Z" }, + { url = "https://files.pythonhosted.org/packages/8c/56/93b2310891589063e0226cb07fe05e08fbdb3f409f80f9edefb1cebea84e/dearpygui-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:272cc52c66bc5a4a332f13a3af5e603070d47ca7cc4976e6a8c247594e2b3dd0", size = 1943067, upload-time = "2026-05-01T22:45:52.255Z" }, +] + +[[package]] +name = "dpg-map" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "dearpygui" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dearpygui", specifier = ">=2.3.1" }, + { name = "pillow", specifier = ">=12.2.0" }, + { name = "platformdirs", specifier = ">=4.9.6" }, + { name = "requests", specifier = ">=2.34.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.409" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "ruff", specifier = ">=0.15.14" }, +] + +[[package]] +name = "idna" +version = "3.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz", hash = "sha256:d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", size = 203770, upload-time = "2026-05-22T00:16:18.781Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, + { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, + { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, + { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, + { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +]