Initial commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
719
ARCHITECTURE.md
Normal file
719
ARCHITECTURE.md
Normal 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
554
FEATURES.md
Normal 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 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.
|
||||
764
STEPS.md
Normal file
764
STEPS.md
Normal 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 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
|
||||
```
|
||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal 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
2
src/dpg_map/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def main() -> None:
|
||||
print("Hello from dpg-map!")
|
||||
289
uv.lock
generated
Normal file
289
uv.lock
generated
Normal 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" },
|
||||
]
|
||||
Reference in New Issue
Block a user