Skip to content

Modernize packaging, tooling, and CI; refresh the GraphQL schema - #40

Draft
parrot-tailor wants to merge 14 commits into
jhansche:mainfrom
parrot-tailor:modernize-tooling
Draft

Modernize packaging, tooling, and CI; refresh the GraphQL schema#40
parrot-tailor wants to merge 14 commits into
jhansche:mainfrom
parrot-tailor:modernize-tooling

Conversation

@parrot-tailor

Copy link
Copy Markdown
Contributor

Brings the project onto a modern Python toolchain — pyproject packaging, ruff, pyright, a Makefile-driven gate, and CI — restructures and expands the tests, refreshes the GraphQL schema, and standardizes fixtures on JSON. Pure maintenance: no public symbol is renamed or moved and no signature changes, so the API that downstream consumers (e.g. ha-birdbuddy) import is unchanged.

Commits

  1. fca98ca Adopt canonical GitHub Python.gitignore — Replace the ad-hoc ignore file with the upstream github/gitignore Python template plus a few project-specific entries (junit.xml, IDE dirs, local API payload dumps).
  2. 775468e Migrate packaging from setup.py to pyproject.toml — PEP 621 metadata on the setuptools backend, scoped to birdbuddy* so tests/ no longer ships in the wheel; dev/test/publish tooling folded into a [dev] extra; interpreter pinned via .python-version (3.10.20).
  3. 0ce1450 Add ruff, pyright, pytest and coverage config plus Makefile — Configure the toolchain (ruff google ruleset at line-length 79 / py310, pytest asyncio + coverage) and add pyrightconfig.json, .markdownlint.yaml and a Makefile (deps/format/lint/test/check/schema/build/publish/clean). Lint is intentionally red until the cleanup commit.
  4. c69e431 Add the MIT license — Add LICENSE and wire it into the packaging metadata.
  5. 4dbcaff Resolve all ruff and pyright findings; adopt Google-style docstrings — Bring the tree to a clean gate without changing valid-input behavior: formatting + absolute imports, Google Args/Returns/Raises docstrings, type annotations, asserts removed from source, a PEP 561 py.typed marker.
  6. 30ed324 Rework schema tooling, refresh the dump, add a drift guard — Move dump_schema.py to scripts/ and schema.json to tests/fixtures/ (refreshed from live introspection), delete the stale 2023 dump, and add test_schema.py asserting the schema still defines every type the queries use.
  7. 11ebc8d Document the sighting_create schema drift; add a payload dumper — The drift guard shows the API no longer defines SightingCreateInput / SightingCreateCheckProgressInput, so sighting_create* are stale; document that (allowlisted in the test) and add scripts/dump_payloads.py to capture real responses as sanitized fixtures.
  8. c4db8d6 Guard the postcard argument against an unbound localreanalyze_postcard / sighting_from_postcard had no else, leaving postcard_id possibly-unbound for non-str/FeedNode input; raise TypeError instead. Valid inputs behave identically.
  9. d82b830 Restructure client tests into test_client.py — Merge the issue/feature-named test files into a source-module-named file and parameterize the duplicated best-guess / check-progress cases.
  10. 48385fb Characterize the sightings model logic — Add test_sightings.py pinning the current postcard finishing logic (strategies, highest-confidence matches, token decoding, SightingType) as a safety net for the follow-up rewrite.
  11. 3a23c9a Modernize the CI workflow and add dependabot — Rewrite the workflow to run make deps + make check across a Python 3.10–3.14 matrix, bump actions, add least-privilege permissions, drop the weekly cron, and add dependabot for pip + github-actions.
  12. 916cb11 Restructure the README and document the dev workflow — Add a description, status shields (PyPI/build/maintenance/release/license), and Installation/Usage/Development/Releasing sections; fix the broken import/instantiation examples; document make build/make publish.
  13. 58633dc Add basic tests for the birds, user, feed, media and feeder models — Per-module tests for the previously-untested models (built from dict fixtures, no network); TODO-mark the dormant power_profile default bug.
  14. c729623 Standardize test fixtures on JSON and add a sanitized real payload — Convert the YAML fixture to JSON and drop pyyaml; add api_payloads.json (a sanitized 2026-07 capture) with realistic model tests; TODO-mark the location per-type mismatch.

Issues and bugs discovered

Surfaced by the schema refresh, a cross-check of the Bird Buddy app, and the new tests.

Behavioral — pinned here, to fix in a follow-up PR

  • sighting_create / sighting_create_check_progress are broken by schema drift. The API dropped SightingCreateInput / SightingCreateCheckProgressInput. Surfaced by the new drift test, allowlisted in test_schema.py, and captured as a real error in the sanitized fixture. Upstream: pybirdbuddy#29, pybirdbuddy#30 (closed), downstream symptom ha-birdbuddy#98.
  • Feeder.power_profile default mismatch. Defaults to "STANDARD", which isn't the enum value "STANDARD_MODE", so a feeder missing powerProfile resolves to UNKNOWN. Dormant — owner responses always carry the field. TODO in feeder.py. No existing upstream issue.
  • Feeder.location per-type mismatch. Reads flat locationCity/locationCountry, but FeederForOwner nests location{city,country} (member/public use the flat shape; neither deprecated), so owner feeders return (None, None). TODO in feeder.py. No existing upstream issue.

Fixed in this PR (safe, no contract change)

  • Unbound postcard_id for non-str/FeedNode input → now a clear TypeError (c4db8d6).
  • tests/ shipped in the wheel via find_packages() → packaging scoped to birdbuddy* (775468e).
  • Broken README examples (from birdbuddy import BirdBuddy, bare BirdBuddy class) → corrected (916cb11).

Out of scope (not pybirdbuddy)

  • ha-birdbuddy#95 — recent-visitor state lag is an ha-birdbuddy coordinator/state-timing bug; unrelated to and unaffected by this PR.

Testing

make check (ruff, ruff format, markdownlint, pyright, 52 pytest) passes locally on Python 3.10.20; CI runs the same gate across the Python 3.10–3.14 matrix. Coverage: 62% → 71% (11 → 52 tests).

% make check
Running format check, lint, and typecheck...
All checks passed!
30 files already formatted
markdownlint-cli2 v0.23.0 (markdownlint v0.41.0)
Finding: *.md
Linting: 1 file(s)
Summary: 0 error(s)
0 errors, 0 warnings, 0 informations
Running the full test suite...
....................................................                                                                 [100%]
====================================================== tests coverage ======================================================
____________________________________ coverage: platform darwin, python 3.10.20-final-0 _____________________________________

Name                            Stmts   Miss  Cover   Missing
-------------------------------------------------------------
birdbuddy/__init__.py               3      0   100%
birdbuddy/birds.py                  9      0   100%
birdbuddy/client.py               377    229    39%   87-98, 113-115, 119, 123-124, 126-127, 140-162, 174-197, 228, 246, 251-261, 265, 269, 275, 280, 297-299, 317-345, 363-393, 418-439, 458-471, 482-483, 491, 516-519, 552-572, 599-600, 642-652, 665, 674-687, 702-712, 791-795, 827-844, 857-865, 882-911, 925-961, 974-1006, 1019-1036, 1046-1055, 1066-1074, 1085-1086, 1091-1094
birdbuddy/const.py                  1      0   100%
birdbuddy/exceptions.py            31      3    90%   61, 82-83
birdbuddy/feed.py                  75      0   100%
birdbuddy/feeder.py               140     13    91%   131, 141, 146, 156, 161, 166, 191-194, 213-214, 223-224, 256
birdbuddy/media.py                 59      1    98%   63
birdbuddy/queries/__init__.py       2      0   100%
birdbuddy/queries/auth.py           2      0   100%
birdbuddy/queries/birds.py          9      0   100%
birdbuddy/queries/debug.py          1      0   100%
birdbuddy/queries/feeder.py         6      0   100%
birdbuddy/queries/me.py             4      0   100%
birdbuddy/sightings.py            171     16    91%   41, 43, 46, 93, 101, 160-164, 168, 194-195, 203, 330, 334, 368-369
birdbuddy/user.py                  11      0   100%
-------------------------------------------------------------
TOTAL                             901    262    71%
Coverage HTML written to dir htmlcov
52 passed in 0.20s
All checks passed.

Follow-up (separate PR, based on this branch)

A second PR, branched off this one, will migrate the postcard flow to postcardCollect — the fix for the sighting_create drift above and ha-birdbuddy#98 — fix the power_profile and location model bugs, update the characterization tests deliberately, and bump 0.0.210.1.0.

Replace the drifted .gitignore with a byte-for-byte copy of upstream
github/gitignore Python.gitignore, then append a project-specific
section. Upstream adds .ruff_cache/ and comments out .python-version so
the pinned interpreter can be checked in.

Preserve the maintainer's explicit editor ignores (.idea/, *.iml,
.vscode, *.whl) and add junit.xml, emitted by the pytest config.
Adopt PEP 621 metadata with the setuptools build backend. Scope
packages to birdbuddy* so tests/ is no longer shipped in the wheel
(find_packages() previously packaged it). Move dev/test/publish tooling
into an optional [dev] extra: pytest stack, ruff, pyright, build, twine.

Pin the dev/lint interpreter via .python-version (3.10.20, the support
floor). Remove setup.py, requirements.txt and requirements.test.txt.

Publishing is now: python -m build && twine upload dist/*
Configure the toolchain in pyproject.toml:
- ruff: full google-style ruleset at line-length 79, target py310, with
  absolute-import enforcement (TID252 + ban-relative-imports = all)
- pytest: asyncio_mode = auto and coverage/junit addopts
- coverage: source = birdbuddy, omit tests

Add pyrightconfig.json (py310, tests exec env), .markdownlint.yaml
(matches the linters used in CI), and a Makefile with deps/format/lint/
test/check/clean plus build, publish (python -m build + twine) and
schema targets.

Lint is intentionally red until the cleanup commit; the source is not
yet formatted or annotated.
Proposing to license this project under the MIT license.

Fixes jhansche#32.
Bring the tree to a clean gate (make check: ruff, ruff format,
markdownlint, pyright, pytest) without changing valid-input behavior:

- Format to line-length 79 and sort imports; convert package-relative
  imports to absolute (TID252, ban-relative-imports = all). Symbol
  imports are kept (not module-qualified).
- Rewrite docstrings to Google style: Args/Returns/Raises on functions
  with parameters, attribute-style one-liners on properties; convert the
  old reStructuredText :param blocks.
- Add type annotations; refine loose return types (e.g. str | None,
  datetime | None), fix UserDict[str, any] (the builtin) to typing.Any,
  and correct GraphqlError.error_code to str | None.
- Remove asserts from source (they are reserved for tests): precondition
  asserts become explicit raises, and the two assert-await-refresh cases
  become plain await calls so the request is not tied to an assertion.
- Enable the S101 (assert) and FA (future-annotations) ruff rules;
  ignore E501 for GraphQL query strings; keep the N818 noqa on
  CompositeException (public name, no rename).
- Add a PEP 561 py.typed marker (shipped in the wheel) so downstream
  consumers can use the annotations.
- Reflow README prose to satisfy markdownlint.

make check passes on 3.10.20; the frozen public API imports unchanged.
Move the schema artifacts out of the repo root:
- scripts/dump_schema.py writes the fixture via a __file__-relative path
  (run with make schema)
- tests/fixtures/schema.json, refreshed from the live introspection
  endpoint
- remove the stale schema-20230225.txt

Add tests/test_schema.py, which derives the GraphQL types the library's
queries use (input types from birdbuddy.queries, object types from the
SightingType enum) and asserts the committed schema still defines them.

The guard immediately surfaces real drift: the refreshed schema no
longer defines SightingCreateInput or SightingCreateCheckProgressInput,
so make check fails here -- the sighting_create and
sighting_create_check_progress queries are stale against the current
API.
The schema drift guard flags that the API dropped SightingCreateInput and
SightingCreateCheckProgressInput. Exempt them via a documented
_KNOWN_MISSING allowlist: the generic sightingCreate flow was removed with
no equivalent (the app now collects via postcardCollect), so
sighting_create / sighting_create_check_progress are non-functional but are
kept for backwards compatibility (see jhansche#29).

Add scripts/dump_payloads.py: a maintainer tool that logs in via
BB_EMAIL/BB_PASSWORD and dumps the postcard -> sighting flow (capturing
server errors) to a git-ignored *.dump.json, for debugging and for
building sanitized test fixtures.
reanalyze_postcard and sighting_from_postcard branched on str / FeedNode
with no else, leaving postcard_id unbound (UnboundLocalError) for any
other input type. Add an else that raises a clear TypeError. Valid str /
FeedNode inputs behave identically. Add a guard test.
Merge the issue/feature-named test_issue_40.py and test_sighting_create.py
into a source-module-named test_client.py. Parameterize the best-guess /
anomaly-correction finish cases and the in-progress / completed
check-progress cases; hoist the repeated UUIDs into named constants.
Rename the issue_40 fixture and issue-40.yaml to postcard_sighting.
Behavior and coverage are unchanged.
Add tests/test_sightings.py pinning the current behavior of the complex
sighting-report logic the postcard finishing flow relies on: SightingType
classification, token_json (plain / signed / malformed), the finishing
strategies (recognized, recognized-species propagation, mystery fallback),
and highest_confidence_matches. Gives a safety net for a later rewrite.
Rewrite .github/workflows/python-package.yml as a single check job that
runs make deps then make check across a Python 3.10-3.14 matrix, so CI
exercises the same ruff, ruff format, markdownlint, pyright and pytest
gate as local development.

Bump to actions/checkout@v7 and actions/setup-python@v6, add
contents: read permissions, and drop the inherited weekly cron in favor
of push, pull_request and workflow_dispatch triggers.

Add .github/dependabot.yml to track pip and github-actions updates,
grouped weekly.
Give the README a clear structure and fix its broken examples:

- Add the project description and status shields (PyPI version, build,
  maintenance, GitHub release, license).
- Split the content into Installation, Usage, Translations,
  Development and Releasing sections.
- Fix the Translations example: import BirdBuddy from birdbuddy.client
  (the package root only exports LOGGER/VERBOSE) and instantiate it with
  credentials instead of referencing the bare class.
- Document the development workflow: pyenv, make deps and the make
  targets (each self-activates the venv), plus pip install -e '.[dev]'.
- Document the PyPI release process: bump the version, then make build
  (python -m build -> sdist + wheel) and make publish (twine check +
  upload), including how twine reads its credentials.

The FeederForPrivate fragment is unchanged; it is still the type the
library queries and is present in the refreshed schema.
Cover the model classes that had no direct tests, built from dict
fixtures with no network:

- Species and BirdBuddyUser property mapping.
- Feed / FeedNode / FeedNodeType: type resolution and the Unknown
  fallback, datetime parsing, and Feed.filter / newest_edge.
- Media / Collection / is_media_expired: signed-URL expiry, image vs
  video, and collection species/visits/cover media.
- Feeder enums (known values + UNKNOWN fallback), Signal, Battery,
  Feeder, and FeederUpdateStatus.

Raises total coverage from 63% to 71%.

Pin one dormant quirk (and mark it with a TODO) rather than change
behavior here: Feeder.power_profile defaults to "STANDARD", which does
not match the PowerProfile enum value ("STANDARD_MODE"), so a Feeder
missing powerProfile resolves to UNKNOWN. This never surfaces in
practice -- only owner responses carry powerProfile and they always
include it -- so ha-birdbuddy is unaffected. It is an independent
bugfix, not part of the 0.1.0 breaking changes.
Convert postcard_sighting.yaml to JSON and load fixtures via json; drop
the now-unused pyyaml dev dependency. The API, the payload dumper and
schema.json are all JSON, so the fixtures follow suit.

Sanitize postcard_sighting.json: remap every account identifier (feeder,
sighting and media ids) to generated UUIDs and re-encode its JWT
reportToken with scrubbed userId/feederId. The token's reportItems, which
the finish-flow tests rely on, are preserved, and test_client.py's
expected constants are remapped to match.

Add tests/fixtures/api_payloads.json: a sanitized capture of live
responses (2026-07). Identifying values (UUIDs, media URLs, feeder name,
serial number, location, invite/report tokens) are replaced; structure,
enum values, timestamps and the sighting_create error are preserved.

Add tests/test_api_payloads.py, exercising Feeder, FeedNode,
PostcardSighting/SightingReport and the recognized finish strategy
against the real payload, and pinning the live sighting_create drift
(Unknown type "SightingCreateInput", jhansche#29).

Mark a second dormant model bug with a TODO: FeederForOwner nests
location{city,country} while FeederForMember/Public use flat
locationCity/locationCountry (neither deprecated), so Feeder.location
returns (None, None) for owner feeders.

@jhansche jhansche left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the substantial effort here — there’s a lot of useful work in this PR.

My main concern is scope: it combines packaging, build tooling, CI, schema refreshes, fixture changes, and test restructuring in one very large change set. That makes it hard to review and increases merge risk. I’d be much more comfortable if this were split into smaller PRs, especially separating pure repo/tooling changes from functional/test changes.

Each of these could be independent PRs with a much tighter scope, making them easier to review, easier to identify regressions, and easier to revert if necessary.

And I haven't even gotten to the potential breaking changes yet, in the next PR 😮💨 (hint: I'm going to ask that all changes be made backwards compatible, so that merging pybirdbuddy changes doesn't break ha-birdbuddy, and the ha-birdbuddy changes don't introduce user-facing breaking changes without some kind of deprecation warning period that encourages users to switch to the simpler events). Also note that ha-birdbuddy intentionally runs CI checks against the pybirdbuddy main branch, and not a pinned version.

@parrot-tailor

Copy link
Copy Markdown
Contributor Author

Thanks @jhansche! Will split this up. For the breaking changes in the follow-up PR, those reflect breaking changes that were introduced on the Bird Buddy side. Normally I wouldn't have included a version bump on that one (that's owned by the maintainer, aka you), but let me rethink how I can handle those better so you (and users) can adopt them without breaking ha-birdbuddy in the process 🙇‍♂️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants