Initial commit

This commit is contained in:
2026-05-22 18:14:35 +02:00
commit d6900dbf80
9 changed files with 2368 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

719
ARCHITECTURE.md Normal file
View File

@@ -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
```

554
FEATURES.md Normal file
View File

@@ -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 2060 Hz from another thread.
7. Dragging works while a trajectory is updated at 1030 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.

0
README.md Normal file
View File

764
STEPS.md Normal file
View File

@@ -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 2060 Hz do not break dragging
- trajectory updates at 1030 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
```

29
pyproject.toml Normal file
View File

@@ -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",
]

2
src/dpg_map/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
def main() -> None:
print("Hello from dpg-map!")

289
uv.lock generated Normal file
View File

@@ -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" },
]