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.
Files changed (85) hide show
  1. archibald-1.0.0/.claude/plans/release-plan.md +161 -0
  2. archibald-1.0.0/.github/workflows/ci.yml +35 -0
  3. archibald-1.0.0/.github/workflows/docs.yml +24 -0
  4. archibald-1.0.0/.github/workflows/publish.yml +34 -0
  5. archibald-1.0.0/.gitignore +25 -0
  6. archibald-1.0.0/.pre-commit-config.yaml +7 -0
  7. archibald-1.0.0/.python-version +1 -0
  8. archibald-1.0.0/CHANGELOG.md +69 -0
  9. archibald-1.0.0/CLAUDE.md +66 -0
  10. archibald-1.0.0/CONTRIBUTING.md +80 -0
  11. archibald-1.0.0/LICENSE +7 -0
  12. archibald-1.0.0/PKG-INFO +225 -0
  13. archibald-1.0.0/README.md +194 -0
  14. archibald-1.0.0/docs/api/auth.md +7 -0
  15. archibald-1.0.0/docs/api/client.md +3 -0
  16. archibald-1.0.0/docs/api/exceptions.md +50 -0
  17. archibald-1.0.0/docs/api/models.md +9 -0
  18. archibald-1.0.0/docs/api/services.md +17 -0
  19. archibald-1.0.0/docs/index.md +46 -0
  20. archibald-1.0.0/docs/usage.md +401 -0
  21. archibald-1.0.0/mkdocs.yml +37 -0
  22. archibald-1.0.0/pyproject.toml +67 -0
  23. archibald-1.0.0/scripts/query_layer.py +131 -0
  24. archibald-1.0.0/scripts/smoke_test_edits.py +232 -0
  25. archibald-1.0.0/src/archibald/__init__.py +65 -0
  26. archibald-1.0.0/src/archibald/auth/__init__.py +12 -0
  27. archibald-1.0.0/src/archibald/auth/base.py +57 -0
  28. archibald-1.0.0/src/archibald/auth/no_auth.py +24 -0
  29. archibald-1.0.0/src/archibald/auth/user_token.py +157 -0
  30. archibald-1.0.0/src/archibald/client.py +147 -0
  31. archibald-1.0.0/src/archibald/errors.py +62 -0
  32. archibald-1.0.0/src/archibald/exceptions.py +80 -0
  33. archibald-1.0.0/src/archibald/models/__init__.py +12 -0
  34. archibald-1.0.0/src/archibald/models/apply_edits_result.py +107 -0
  35. archibald-1.0.0/src/archibald/models/fields_result.py +137 -0
  36. archibald-1.0.0/src/archibald/models/query_result.py +113 -0
  37. archibald-1.0.0/src/archibald/operations/__init__.py +9 -0
  38. archibald-1.0.0/src/archibald/operations/apply_edits.py +276 -0
  39. archibald-1.0.0/src/archibald/operations/query.py +229 -0
  40. archibald-1.0.0/src/archibald/py.typed +0 -0
  41. archibald-1.0.0/src/archibald/serializers/__init__.py +1 -0
  42. archibald-1.0.0/src/archibald/serializers/_coercions.py +292 -0
  43. archibald-1.0.0/src/archibald/serializers/_features.py +237 -0
  44. archibald-1.0.0/src/archibald/serializers/_geometry.py +94 -0
  45. archibald-1.0.0/src/archibald/services/__init__.py +17 -0
  46. archibald-1.0.0/src/archibald/services/base.py +69 -0
  47. archibald-1.0.0/src/archibald/services/feature_service.py +9 -0
  48. archibald-1.0.0/src/archibald/services/layers/__init__.py +0 -0
  49. archibald-1.0.0/src/archibald/services/layers/base.py +128 -0
  50. archibald-1.0.0/src/archibald/services/layers/feature_layer.py +281 -0
  51. archibald-1.0.0/src/archibald/services/layers/map_layer.py +15 -0
  52. archibald-1.0.0/src/archibald/services/map_service.py +9 -0
  53. archibald-1.0.0/tests/__init__.py +0 -0
  54. archibald-1.0.0/tests/auth/__init__.py +0 -0
  55. archibald-1.0.0/tests/auth/test_base.py +76 -0
  56. archibald-1.0.0/tests/auth/test_no_auth.py +50 -0
  57. archibald-1.0.0/tests/auth/test_user_token.py +313 -0
  58. archibald-1.0.0/tests/conftest.py +196 -0
  59. archibald-1.0.0/tests/helpers.py +165 -0
  60. archibald-1.0.0/tests/integration/__init__.py +0 -0
  61. archibald-1.0.0/tests/integration/conftest.py +90 -0
  62. archibald-1.0.0/tests/integration/test_feature_layer.py +109 -0
  63. archibald-1.0.0/tests/models/__init__.py +0 -0
  64. archibald-1.0.0/tests/models/test_apply_edits_result.py +296 -0
  65. archibald-1.0.0/tests/models/test_fields_result.py +214 -0
  66. archibald-1.0.0/tests/models/test_query_result.py +332 -0
  67. archibald-1.0.0/tests/operations/__init__.py +0 -0
  68. archibald-1.0.0/tests/operations/test_apply_edits.py +602 -0
  69. archibald-1.0.0/tests/operations/test_query.py +287 -0
  70. archibald-1.0.0/tests/serializers/__init__.py +0 -0
  71. archibald-1.0.0/tests/serializers/test_coercions.py +480 -0
  72. archibald-1.0.0/tests/serializers/test_features.py +206 -0
  73. archibald-1.0.0/tests/serializers/test_geometry.py +263 -0
  74. archibald-1.0.0/tests/services/__init__.py +0 -0
  75. archibald-1.0.0/tests/services/layers/__init__.py +0 -0
  76. archibald-1.0.0/tests/services/layers/test_base.py +243 -0
  77. archibald-1.0.0/tests/services/layers/test_feature_layer.py +284 -0
  78. archibald-1.0.0/tests/services/layers/test_map_layer.py +23 -0
  79. archibald-1.0.0/tests/services/test_base.py +122 -0
  80. archibald-1.0.0/tests/services/test_feature_service.py +19 -0
  81. archibald-1.0.0/tests/services/test_map_service.py +19 -0
  82. archibald-1.0.0/tests/test_client.py +319 -0
  83. archibald-1.0.0/tests/test_errors.py +129 -0
  84. archibald-1.0.0/tests/test_exceptions.py +125 -0
  85. 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,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.13
4
+ hooks:
5
+ - id: ruff-format
6
+ - id: ruff
7
+ args: [--fix]
@@ -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.
@@ -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.