archibald 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- archibald-1.0.0/.claude/plans/release-plan.md +161 -0
- archibald-1.0.0/.github/workflows/ci.yml +35 -0
- archibald-1.0.0/.github/workflows/docs.yml +24 -0
- archibald-1.0.0/.github/workflows/publish.yml +34 -0
- archibald-1.0.0/.gitignore +25 -0
- archibald-1.0.0/.pre-commit-config.yaml +7 -0
- archibald-1.0.0/.python-version +1 -0
- archibald-1.0.0/CHANGELOG.md +69 -0
- archibald-1.0.0/CLAUDE.md +66 -0
- archibald-1.0.0/CONTRIBUTING.md +80 -0
- archibald-1.0.0/LICENSE +7 -0
- archibald-1.0.0/PKG-INFO +225 -0
- archibald-1.0.0/README.md +194 -0
- archibald-1.0.0/docs/api/auth.md +7 -0
- archibald-1.0.0/docs/api/client.md +3 -0
- archibald-1.0.0/docs/api/exceptions.md +50 -0
- archibald-1.0.0/docs/api/models.md +9 -0
- archibald-1.0.0/docs/api/services.md +17 -0
- archibald-1.0.0/docs/index.md +46 -0
- archibald-1.0.0/docs/usage.md +401 -0
- archibald-1.0.0/mkdocs.yml +37 -0
- archibald-1.0.0/pyproject.toml +67 -0
- archibald-1.0.0/scripts/query_layer.py +131 -0
- archibald-1.0.0/scripts/smoke_test_edits.py +232 -0
- archibald-1.0.0/src/archibald/__init__.py +65 -0
- archibald-1.0.0/src/archibald/auth/__init__.py +12 -0
- archibald-1.0.0/src/archibald/auth/base.py +57 -0
- archibald-1.0.0/src/archibald/auth/no_auth.py +24 -0
- archibald-1.0.0/src/archibald/auth/user_token.py +157 -0
- archibald-1.0.0/src/archibald/client.py +147 -0
- archibald-1.0.0/src/archibald/errors.py +62 -0
- archibald-1.0.0/src/archibald/exceptions.py +80 -0
- archibald-1.0.0/src/archibald/models/__init__.py +12 -0
- archibald-1.0.0/src/archibald/models/apply_edits_result.py +107 -0
- archibald-1.0.0/src/archibald/models/fields_result.py +137 -0
- archibald-1.0.0/src/archibald/models/query_result.py +113 -0
- archibald-1.0.0/src/archibald/operations/__init__.py +9 -0
- archibald-1.0.0/src/archibald/operations/apply_edits.py +276 -0
- archibald-1.0.0/src/archibald/operations/query.py +229 -0
- archibald-1.0.0/src/archibald/py.typed +0 -0
- archibald-1.0.0/src/archibald/serializers/__init__.py +1 -0
- archibald-1.0.0/src/archibald/serializers/_coercions.py +292 -0
- archibald-1.0.0/src/archibald/serializers/_features.py +237 -0
- archibald-1.0.0/src/archibald/serializers/_geometry.py +94 -0
- archibald-1.0.0/src/archibald/services/__init__.py +17 -0
- archibald-1.0.0/src/archibald/services/base.py +69 -0
- archibald-1.0.0/src/archibald/services/feature_service.py +9 -0
- archibald-1.0.0/src/archibald/services/layers/__init__.py +0 -0
- archibald-1.0.0/src/archibald/services/layers/base.py +128 -0
- archibald-1.0.0/src/archibald/services/layers/feature_layer.py +281 -0
- archibald-1.0.0/src/archibald/services/layers/map_layer.py +15 -0
- archibald-1.0.0/src/archibald/services/map_service.py +9 -0
- archibald-1.0.0/tests/__init__.py +0 -0
- archibald-1.0.0/tests/auth/__init__.py +0 -0
- archibald-1.0.0/tests/auth/test_base.py +76 -0
- archibald-1.0.0/tests/auth/test_no_auth.py +50 -0
- archibald-1.0.0/tests/auth/test_user_token.py +313 -0
- archibald-1.0.0/tests/conftest.py +196 -0
- archibald-1.0.0/tests/helpers.py +165 -0
- archibald-1.0.0/tests/integration/__init__.py +0 -0
- archibald-1.0.0/tests/integration/conftest.py +90 -0
- archibald-1.0.0/tests/integration/test_feature_layer.py +109 -0
- archibald-1.0.0/tests/models/__init__.py +0 -0
- archibald-1.0.0/tests/models/test_apply_edits_result.py +296 -0
- archibald-1.0.0/tests/models/test_fields_result.py +214 -0
- archibald-1.0.0/tests/models/test_query_result.py +332 -0
- archibald-1.0.0/tests/operations/__init__.py +0 -0
- archibald-1.0.0/tests/operations/test_apply_edits.py +602 -0
- archibald-1.0.0/tests/operations/test_query.py +287 -0
- archibald-1.0.0/tests/serializers/__init__.py +0 -0
- archibald-1.0.0/tests/serializers/test_coercions.py +480 -0
- archibald-1.0.0/tests/serializers/test_features.py +206 -0
- archibald-1.0.0/tests/serializers/test_geometry.py +263 -0
- archibald-1.0.0/tests/services/__init__.py +0 -0
- archibald-1.0.0/tests/services/layers/__init__.py +0 -0
- archibald-1.0.0/tests/services/layers/test_base.py +243 -0
- archibald-1.0.0/tests/services/layers/test_feature_layer.py +284 -0
- archibald-1.0.0/tests/services/layers/test_map_layer.py +23 -0
- archibald-1.0.0/tests/services/test_base.py +122 -0
- archibald-1.0.0/tests/services/test_feature_service.py +19 -0
- archibald-1.0.0/tests/services/test_map_service.py +19 -0
- archibald-1.0.0/tests/test_client.py +319 -0
- archibald-1.0.0/tests/test_errors.py +129 -0
- archibald-1.0.0/tests/test_exceptions.py +125 -0
- archibald-1.0.0/uv.lock +1740 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Plan: Prepare `archie` for PyPI v1.0.0 Release
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
`archie` is an async-first Python client for ESRI ArcGIS REST APIs. The code is architecturally complete with a full test suite, but it's missing the packaging metadata, documentation, and automation expected of a production PyPI release. This plan addresses the gaps needed to publish a credible v1.0.0.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Tasks
|
|
10
|
+
|
|
11
|
+
### 1. Update `pyproject.toml` metadata
|
|
12
|
+
|
|
13
|
+
**File:** `pyproject.toml`
|
|
14
|
+
|
|
15
|
+
Add/update the following fields under `[project]`:
|
|
16
|
+
|
|
17
|
+
- `version = "1.0.0"`
|
|
18
|
+
- `authors = [{name = "City of Boulder Department of Innovation and Technology", email = "nestlerj@bouldercolorado.gov"}]`
|
|
19
|
+
- `license = {file = "LICENSE"}`
|
|
20
|
+
- `keywords = ["arcgis", "esri", "gis", "rest", "async", "geospatial"]`
|
|
21
|
+
- `classifiers`:
|
|
22
|
+
```toml
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 5 - Production/Stable",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
```
|
|
33
|
+
- `[project.urls]` section:
|
|
34
|
+
```toml
|
|
35
|
+
[project.urls]
|
|
36
|
+
Repository = "https://github.com/cityofboulder/archie" # adjust if needed
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Also update `readme` to use explicit content type:
|
|
40
|
+
```toml
|
|
41
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 2. Write README.md
|
|
47
|
+
|
|
48
|
+
**File:** `README.md` (currently empty)
|
|
49
|
+
|
|
50
|
+
Minimum required content:
|
|
51
|
+
- Package name, one-paragraph description
|
|
52
|
+
- Installation: `pip install archie` or `uv add archie`
|
|
53
|
+
- Quick-start code snippet (create client, authenticate, query a layer)
|
|
54
|
+
- Brief API overview: `ArchieClient`, auth classes, service/layer classes
|
|
55
|
+
- Link to CHANGELOG
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### 3. Update CHANGELOG.md for v1.0.0
|
|
60
|
+
|
|
61
|
+
**File:** `CHANGELOG.md`
|
|
62
|
+
|
|
63
|
+
- Rename `## [Unreleased]` → `## [1.0.0] - 2026-06-01`
|
|
64
|
+
- Add a new empty `## [Unreleased]` section above it
|
|
65
|
+
- Add the compare link at the bottom: `[1.0.0]: https://github.com/.../compare/v0.1.0...v1.0.0`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 4. Add `py.typed` marker
|
|
70
|
+
|
|
71
|
+
**File:** `src/archie/py.typed` (empty file)
|
|
72
|
+
|
|
73
|
+
Required for PEP 561 so that type checkers know this package ships inline types. Also add it to `pyproject.toml` under `[tool.hatch.build.targets.wheel]`:
|
|
74
|
+
```toml
|
|
75
|
+
[tool.hatch.build.targets.wheel]
|
|
76
|
+
include = ["src/archie/py.typed"]
|
|
77
|
+
```
|
|
78
|
+
(Hatchling auto-includes everything under `src/`, so this may already be covered — verify.)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 5. Expand public API in `__init__.py`
|
|
83
|
+
|
|
84
|
+
**File:** `src/archie/__init__.py`
|
|
85
|
+
|
|
86
|
+
Export commonly used symbols so users can do `from archie import FeatureLayer, ArcGISAuth, QueryResult` without digging into submodules:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from archie.client import ArchieClient
|
|
90
|
+
from archie.auth import ArcGISAuth, NoAuth, UserTokenAuth
|
|
91
|
+
from archie.exceptions import (
|
|
92
|
+
ArcGISError,
|
|
93
|
+
TokenExpiredError,
|
|
94
|
+
# ... other user-facing exceptions
|
|
95
|
+
)
|
|
96
|
+
from archie.models import QueryResult, FieldsResult, ApplyEditsResult
|
|
97
|
+
from archie.services import FeatureService, MapService
|
|
98
|
+
from archie.services.layers import FeatureLayer, MapLayer
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"ArchieClient",
|
|
102
|
+
"ArcGISAuth", "NoAuth", "UserTokenAuth",
|
|
103
|
+
"ArcGISError", "TokenExpiredError", ...,
|
|
104
|
+
"QueryResult", "FieldsResult", "ApplyEditsResult",
|
|
105
|
+
"FeatureService", "MapService",
|
|
106
|
+
"FeatureLayer", "MapLayer",
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 6. Add GitHub Actions CI
|
|
113
|
+
|
|
114
|
+
**Files:**
|
|
115
|
+
- `.github/workflows/ci.yml` — run `uv run pytest` on push/PR (Python 3.12, Windows + Ubuntu)
|
|
116
|
+
- `.github/workflows/publish.yml` — publish to PyPI on `v*` tag push using Trusted Publishing (OIDC, no API keys needed)
|
|
117
|
+
|
|
118
|
+
The publish workflow should:
|
|
119
|
+
1. Build with `uv build`
|
|
120
|
+
2. Publish with `uv publish` (or `twine upload dist/*`)
|
|
121
|
+
3. Use `environment: pypi` with PyPI Trusted Publishing configured on the repo
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### 7. (Optional but recommended) Add `pytest-cov` and coverage config
|
|
126
|
+
|
|
127
|
+
Add to `pyproject.toml`:
|
|
128
|
+
```toml
|
|
129
|
+
[tool.coverage.run]
|
|
130
|
+
source = ["archie"]
|
|
131
|
+
branch = true
|
|
132
|
+
|
|
133
|
+
[tool.coverage.report]
|
|
134
|
+
fail_under = 80
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Add `pytest-cov` to `[project.optional-dependencies]` dev group.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Verification
|
|
142
|
+
|
|
143
|
+
1. `uv build` — confirm wheel and sdist build cleanly with no warnings
|
|
144
|
+
2. `pip install dist/archie-1.0.0-*.whl` in a fresh virtualenv, then `python -c "import archie; print(archie.__version__)"` (if `__version__` is added) and `from archie import FeatureLayer`
|
|
145
|
+
3. Check the PyPI preview: `twine check dist/*`
|
|
146
|
+
4. `uv run pytest` — full suite must pass
|
|
147
|
+
5. Review the PyPI upload form / package page preview for completeness
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Priority Order
|
|
152
|
+
|
|
153
|
+
| # | Task | Blocker? |
|
|
154
|
+
|---|------|----------|
|
|
155
|
+
| 1 | pyproject.toml metadata | Yes — bare minimum for PyPI |
|
|
156
|
+
| 2 | README.md | Yes — PyPI page will be blank |
|
|
157
|
+
| 3 | CHANGELOG v1.0.0 header | Yes — semver convention |
|
|
158
|
+
| 4 | py.typed marker | No, but important for type-checking users |
|
|
159
|
+
| 5 | Expanded __init__.py exports | No, but important for usability |
|
|
160
|
+
| 6 | GitHub Actions CI/publish | No, but important before ongoing development |
|
|
161
|
+
| 7 | pytest-cov | Optional |
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: pytest / ${{ matrix.os }} / Python ${{ matrix.python-version }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest]
|
|
16
|
+
python-version: ["3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
27
|
+
uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: uv sync
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: uv run pytest
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Deploy Documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
deploy:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- uses: astral-sh/setup-uv@v5
|
|
23
|
+
- run: uv sync --group docs
|
|
24
|
+
- run: uv run mkdocs gh-deploy --force
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
name: Build and publish to PyPI
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Build
|
|
31
|
+
run: uv build
|
|
32
|
+
|
|
33
|
+
- name: Publish to PyPI
|
|
34
|
+
run: uv publish
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Tests
|
|
13
|
+
.pytest*
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
|
|
17
|
+
# Claude-specific files
|
|
18
|
+
.claude/*.local.*
|
|
19
|
+
|
|
20
|
+
# MkDocs build output
|
|
21
|
+
site/
|
|
22
|
+
|
|
23
|
+
# Code editors
|
|
24
|
+
.vscode/
|
|
25
|
+
.idea/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-06-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add a suite of exceptions split into those that are raised by the ESRI API (`ArcGISError`) and those raised by the `archie` tool itself (`ArchieClientError`).
|
|
15
|
+
- Error handling for quirky ESRI response errors. Functions exist for both parsing ESRI errors (`parse_esri_error()`) and handling ESRI errors for callers as a decorator (`handle_esri_errors()`).
|
|
16
|
+
- Base async auth handler class called `ArcGISAuth` that mandates a `get_token()` method in any subclasses.
|
|
17
|
+
- User token auth flows, where a user supplies a username and password in exchange for a token from ESRI's generateToken endpoint. Implemented as `UserTokenAuth`.
|
|
18
|
+
- Base httpx client that handles authentication, enforces response formatting, and routes get and post requests from any endpoint. Implemented as `ArchieClient`.
|
|
19
|
+
- Base service class that handles service metadata retreival, url validation, and client injection, implemented as `BaseService`.
|
|
20
|
+
- `QueryResult` data class will be returned from all feature layer queries. Has attributes for features returned, field names, whether features are geojson, and the output crs if geometries are returned.
|
|
21
|
+
- `FieldsResult` data class returned from querying a feature layer for field names, types, etc.
|
|
22
|
+
- `QueryOperation` executes queries against a feature layer with automatic pagination. Validates requested field names against the layer's field metadata, builds ESRI REST query parameters with deterministic `OBJECTID ASC` ordering by default, and fans out remaining pages in parallel via `anyio` task groups when `exceededTransferLimit` is returned. Geometry output is always encoded as GeoJSON; CRS defaults to the layer's native spatial reference when `out_sr` is not provided.
|
|
23
|
+
- `FeatureLayer` service class represents a single ESRI FeatureServer layer. Caches layer-level metadata (field definitions, `objectIdField`, `globalIdField`, capabilities) on first access. Exposes `objectid_field()`, `globalid_field()`, `supports_query()`, and `fields()` accessors. Provides a `query()` method that guards against layers lacking query capability and delegates to `QueryOperation`.
|
|
24
|
+
- `NoAuth` auth handler for anonymous/public ArcGIS services. Satisfies the `ArcGISAuth` interface but injects no token.
|
|
25
|
+
- `FeatureService` service class representing a FeatureServer endpoint; validates the service path and provides service-level metadata.
|
|
26
|
+
- `MapService` service class representing a MapServer endpoint; same metadata interface as `FeatureService`.
|
|
27
|
+
- `BaseLayer` abstract base class for individual service layers. Extends `BaseService` with a `layer_id` parameter. Caches layer-level metadata (fields, `objectIdField`, `globalIdField`, capabilities) on first access and exposes `objectid_field()`, `globalid_field()`, `supports_query()`, `fields()`, and `query()`.
|
|
28
|
+
- `MapLayer` read-only layer class for MapServer layers. Inherits MapServer path validation from `MapService` and query capability from `BaseLayer`.
|
|
29
|
+
- `ApplyEditsResult` and `EditResultItem` data classes model the response from `applyEdits`. `ApplyEditsResult` exposes `has_failures`, `failed_adds`, `failed_updates`, and `failed_deletes` properties and supports merging multiple per-batch results via `ApplyEditsResult.merge()`.
|
|
30
|
+
- `ApplyEditsOperation` executes `applyEdits` calls against a `FeatureLayer`. Serializes adds (OBJECTID excluded) and updates (OBJECTID included), normalizes deletes from `DataFrame`, `Series`, or `list[int]`, greedy-packs payloads into ≤ 1.8 MB batches, fans out batch POSTs in parallel via `anyio` task groups, and polls async job status with exponential backoff (0.5–5 s) when the layer declares async edit support.
|
|
31
|
+
- `FeatureLayer.apply_edits()` accepts `adds`, `updates`, and `deletes` (all optional) and delegates to `ApplyEditsOperation`. Issues a `UserWarning` when `rollback_on_failure=True` but the layer does not advertise that capability.
|
|
32
|
+
- `FeatureLayer.append()` convenience method that adds all rows of a `DataFrame` or `GeoDataFrame` as new features.
|
|
33
|
+
- `FeatureLayer.upsert()` queries the layer to identify existing keys, partitions incoming rows into adds and updates, and applies both in a single `apply_edits` call. Raises `InvalidParameterError` if key fields are unknown or produce duplicates.
|
|
34
|
+
- `FeatureLayer.sync()` performs a full dataset replacement keyed on caller-supplied fields: adds absent features, updates matching ones, and deletes features in the layer that have no match in the incoming data.
|
|
35
|
+
- `FieldsResult.filter()` returns a new `FieldsResult` restricted by any combination of field names, ESRI type strings, editability, or nullability. `names` and `types` are mutually exclusive; raises `InvalidParameterError` on unrecognised type strings.
|
|
36
|
+
- `FieldsResult.domain_maps` property returns `{field_name: {"to_name": {code: name}, "to_code": {name: code}}}` for all fields carrying a `codedValue` domain. Fields without a domain or with a non-`codedValue` domain type (e.g. `range`) are excluded.
|
|
37
|
+
- `QueryResult.to_frame()` and `QueryResult.to_geodataframe()` convert query results to `pandas.DataFrame` and `geopandas.GeoDataFrame` respectively. Both accept `parse_dtypes=True` to apply ESRI→pandas type coercions automatically (dates to UTC-aware datetime, integer fields to nullable `Int64`/`Int32`, etc.). `to_geodataframe()` raises `MissingGeometryError` when the result contains no geometry.
|
|
38
|
+
- `QueryResult.apply_coded_values` flag: when `True`, `to_frame()` and `to_geodataframe()` automatically translate coded domain values to their human-readable names after type coercion. Set by passing `apply_coded_values=True` to `BaseLayer.query()`.
|
|
39
|
+
- `geometry_to_esri()` converts shapely geometries (Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon) to ESRI JSON dicts with Z-coordinate support. Returns `None` for null geometries; raises `InvalidParameterError` for unsupported types.
|
|
40
|
+
- `serialize_features()` converts a `DataFrame` or `GeoDataFrame` to ESRI feature dicts, applying outbound type coercions and optionally pairing geometry. Issues a `UserWarning` for columns that are skipped (non-editable or absent from field metadata). Accepts `apply_coded_values=True` to translate human-readable domain names back to their codes before type coercion.
|
|
41
|
+
- `pack_batches()` greedy-packs serialized features and delete IDs into POST body dicts capped at a configurable byte limit (default 1.8 MB).
|
|
42
|
+
- `recode_domains(df, fields, *, direction)` added to `archie.serializers._coercions`. Translates coded domain values bidirectionally: `from_esri` maps codes to names, `to_esri` maps names to codes. Unmapped values pass through unchanged. Emits a `UserWarning` when mapped and unmapped non-null values coexist in the same column, as the result will be mixed-type.
|
|
43
|
+
- `ApplyEditsOperation.execute()` accepts a `poll_timeout` keyword argument (default 300 s) that caps how long the async polling loop will wait for a server-side job to complete before raising `TimeoutError`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `enforce_types(df, fields, *, direction)` added to `archie.serializers._coercions` as a unified ESRI ↔ pandas type-coercion function. `direction="from_esri"` applies `ESRI_TO_PANDAS` conversions (integer fields → nullable dtype, dates → UTC-aware datetime); `direction="to_esri"` applies `PANDAS_TO_ESRI` conversions for serialization. `QueryResult._apply_esri_types` and `_coerce_columns` both delegate to it internally.
|
|
48
|
+
- `BaseLayer.query()`, `FeatureLayer.apply_edits()`, `append()`, `upsert()`, and `sync()` all accept an `apply_coded_values` keyword argument (default `False`) that threads through to the serialization and result-construction layers.
|
|
49
|
+
- `FeatureLayer` now inherits from both `FeatureService` and `BaseLayer` via cooperative MRO; previously it inherited directly from `BaseService`.
|
|
50
|
+
- Layer classes (`FeatureLayer`, `MapLayer`, `BaseLayer`) moved to a `services/layers/` sub-package.
|
|
51
|
+
- `FieldsResult.names` is now a computed property (was a plain attribute). The `editable_only` parameter is removed; use `FieldsResult.filter(editable=True)` instead.
|
|
52
|
+
- `FieldsResult.field_type_map` replaces the former `esri_field_types` attribute.
|
|
53
|
+
- Custom exception classes from `archie.errors` are now used consistently throughout the package in place of built-in Python exceptions.
|
|
54
|
+
- Import paths simplified: all public symbols are re-exported from their respective sub-package `__init__.py` files.
|
|
55
|
+
- `ApplyEditsOperation` now uses server-side async editing only when the payload spans multiple batches. Single-batch payloads always use the synchronous path to avoid unnecessary polling round-trips.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- `ArchieClient` now validates that the base URL ends with `rest/services` at construction time, raising `InvalidServiceURL` on mismatch.
|
|
60
|
+
- Query pagination now correctly detects `exceededTransferLimit` in the response body before fanning out additional page requests.
|
|
61
|
+
- CRS defaults correctly to the layer's native spatial reference when `out_sr` is not supplied to `QueryOperation`.
|
|
62
|
+
- `LayerCapabilityError` is raised (instead of a generic exception) when a caller attempts to query a layer that lacks query capability.
|
|
63
|
+
- `UserTokenAuth` token request now sends `referer` instead of `requestip` in the POST body, matching the ESRI `generateToken` API contract.
|
|
64
|
+
- Async `applyEdits` polling now uses the correct ESRI status strings (`"COMPLETED"` / `"PROCESSING"`) instead of the geoprocessing-task strings (`esriJobSucceeded` / `esriJobFailed`) that were previously used and caused the polling loop to never exit.
|
|
65
|
+
- When an async `applyEdits` job completes, the operation now follows the `resultUrl` returned in the status body to fetch the actual edit results (`addResults`, `updateResults`, `deleteResults`). Previously the status body itself was parsed as the result, which always produced empty result sets.
|
|
66
|
+
- Async polling loop is now bounded by `anyio.fail_after`; previously it could spin indefinitely if the server never returned a terminal status.
|
|
67
|
+
|
|
68
|
+
[Unreleased]: https://github.com/cityofboulder/archie/compare/v1.0.0...HEAD
|
|
69
|
+
[1.0.0]: https://github.com/cityofboulder/archie/releases/tag/v1.0.0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project overview
|
|
4
|
+
|
|
5
|
+
`archie` is an async-first Python client for ESRI ArcGIS REST APIs, built around dependency injection and modern analysis libraries. The stack is `httpx`, `anyio`, `pydantic`, `pytest` + `pytest-anyio` + `pytest-mock`, `pandas`, `geopandas`.
|
|
6
|
+
|
|
7
|
+
Layers (bottom to top):
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
auth → token acquisition and header injection
|
|
11
|
+
client → HTTP methods, error handling, format enforcement
|
|
12
|
+
operations → ESRI-idiomatic calls (query, applyEdits, addAttachment, ...)
|
|
13
|
+
models → Models returned by operation calls (QueryResult, ApplyEditsResult, ...)
|
|
14
|
+
services → cohesive service APIs (FeatureLayer, FeatureService, MapService, ...)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Project structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
archie/
|
|
22
|
+
auth/
|
|
23
|
+
models/
|
|
24
|
+
operations/
|
|
25
|
+
services/
|
|
26
|
+
client.py
|
|
27
|
+
errors.py
|
|
28
|
+
exceptions.py
|
|
29
|
+
tests/
|
|
30
|
+
auth/
|
|
31
|
+
models/
|
|
32
|
+
operations/
|
|
33
|
+
services/
|
|
34
|
+
helpers.py
|
|
35
|
+
conftest.py
|
|
36
|
+
pyproject.toml
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Coding conventions
|
|
40
|
+
|
|
41
|
+
- Surface tradeoffs explicitly; do not assume or hide confusion
|
|
42
|
+
- Minimum code that solves the problem; nothing speculative
|
|
43
|
+
- Touch only what is necessary; clean up only your own mess
|
|
44
|
+
- Leverage existing library plumbing wherever possible
|
|
45
|
+
- No inline comments unless truly non-obvious
|
|
46
|
+
- Always add docstrings to non-test functions and methods
|
|
47
|
+
|
|
48
|
+
## Testing conventions
|
|
49
|
+
|
|
50
|
+
- Framework: `pytest` + `pytest-mock`; never `unittest`
|
|
51
|
+
- Async tests: `@pytest.mark.anyio` (or `anyio_mode = "auto"` in `pyproject.toml`)
|
|
52
|
+
- AAA format with blank lines separating Arrange / Act / Assert
|
|
53
|
+
- Do not write tests until the underlying code change is confirmed
|
|
54
|
+
- When mocks become excessive, flag it and suggest a refactor rather than adding more
|
|
55
|
+
- Fixtures should go in the root `tests/conftest.py` file
|
|
56
|
+
- Helper functions and globals should go in the `tests/helpers.py` file
|
|
57
|
+
- Prioritize parametrizing tests where logical
|
|
58
|
+
- Parametrized tests always include `ids=`
|
|
59
|
+
- Leverage existing fixtures and helpers wherever possible; if either need tweaks in order to make new tests work, prefer updating and verify that they will still work elsewhere.
|
|
60
|
+
|
|
61
|
+
## Architectural decisions
|
|
62
|
+
|
|
63
|
+
- Async-first throughout; use `anyio` primitives (not `asyncio` directly)
|
|
64
|
+
- `@handle_esri_errors` is applied at `_request`, not at the operations layer
|
|
65
|
+
- ESRI format param (`f=json`) is enforced by the client on every request; `f=geojson` is the only exception
|
|
66
|
+
- Per-feature edit errors (`applyEdits`) belong in `ApplyEditsResult` at the endpoint layer, not in the client
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Contributing to archie
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- Python 3.12+
|
|
6
|
+
- [`uv`](https://docs.astral.sh/uv/) for dependency management
|
|
7
|
+
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
1. Fork and clone the repository
|
|
11
|
+
2. Install dependencies:
|
|
12
|
+
```sh
|
|
13
|
+
uv sync
|
|
14
|
+
```
|
|
15
|
+
3. Install the pre-commit hooks (runs ruff format and lint on every commit):
|
|
16
|
+
```sh
|
|
17
|
+
pre-commit install
|
|
18
|
+
```
|
|
19
|
+
If you don't have `pre-commit` installed: `uv tool install pre-commit`
|
|
20
|
+
|
|
21
|
+
## Running tests
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv run pytest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Tests marked `integration` hit a mocked HTTP transport and are included by default. To run only unit tests:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv run pytest -m "not integration"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Coverage must stay at or above 80%. The suite will fail if it drops below that threshold.
|
|
34
|
+
|
|
35
|
+
## Code style
|
|
36
|
+
|
|
37
|
+
Ruff handles formatting and linting. The pre-commit hooks run both automatically on every commit. To run them manually:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv run ruff format .
|
|
41
|
+
uv run ruff check .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
All public functions and methods (outside of tests) require docstrings in [Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings).
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
Install the docs dependencies and serve locally:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv sync --group docs
|
|
52
|
+
uv run mkdocs serve
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Documentation is deployed automatically to GitHub Pages when changes are merged to `main` — you don't need to deploy manually.
|
|
56
|
+
|
|
57
|
+
## Opening issues
|
|
58
|
+
|
|
59
|
+
**Bug reports** — include steps to reproduce, expected vs. actual behavior, your Python version, and a stack trace if one is available.
|
|
60
|
+
|
|
61
|
+
**Feature requests** — describe the use case and, if possible, what you'd want the API to look like.
|
|
62
|
+
|
|
63
|
+
For non-trivial changes, open an issue to align on the approach before investing time in a PR.
|
|
64
|
+
|
|
65
|
+
## Submitting a pull request
|
|
66
|
+
|
|
67
|
+
- Open PRs against `main`
|
|
68
|
+
- CI runs the test suite on Ubuntu and Windows — both must pass
|
|
69
|
+
- Keep PRs focused; one logical change per PR
|
|
70
|
+
|
|
71
|
+
## Releases (maintainers only)
|
|
72
|
+
|
|
73
|
+
Releases are triggered by pushing a version tag:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
git tag v1.1.0
|
|
77
|
+
git push --tags
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The publish workflow builds the package and pushes it to PyPI automatically via OIDC — no manual upload or API token required.
|
archibald-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Department of Innovation and Technology, City of Boulder
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|