Skip to content

Migrate the postcard flow to postcardCollect; remove the legacy sighting API - #41

Draft
parrot-tailor wants to merge 35 commits into
jhansche:mainfrom
parrot-tailor:release-v0.1.0
Draft

Migrate the postcard flow to postcardCollect; remove the legacy sighting API#41
parrot-tailor wants to merge 35 commits into
jhansche:mainfrom
parrot-tailor:release-v0.1.0

Conversation

@parrot-tailor

@parrot-tailor parrot-tailor commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Important

Depends on #40. This branch is cut from modernize-tooling (#40) but targets main — a fork branch can't be a PR base, so both PRs base on main. Until #40 merges, the diff and GitHub's commit list here include #40's commits; review/merge #40 first, then this rebases cleanly onto main. The Commits section below lists only the v0.1.0 work (#40's commits are documented in #40).

Migrates the postcard flow to postcardCollect, removes the legacy sighting/report API outright, paginates the collection and feed endpoints, fixes the power_profile and location model bugs, and bumps 0.0.210.1.0. This is the follow-up PR I mentioned in #40. Unlike #40, it is breaking: the Bird Buddy app has moved entirely to postcardCollect. The sighting_create / sighting_create_check_progress input types are gone from the API; the postcard report-token flow (sightingCreateFromPostcard / sightingReportPostcardFinish) still exists upstream but is dropped here by choice — the app abandoned it, and its reportToken / matchTokens were the ha-birdbuddy #78 event bloat. Inference without collecting is preserved: identify_postcard returns a PostcardAnalysis (recognized species + media, no collect).

Every claim below was verified against the live API with a real account, the committed schema, and the app's JS bundle.

Commits

  1. 30b18a7 Bump version to 0.1.0 — This PR carries breaking changes.
  2. 0b84df2 Capture the live collect flow as a sanitized fixture — Add scripts/dump_payloads.py and collect_flow.json (a sanitized reanalyze + postcardCollect capture) pinning the flow's real behavior.
  3. ef1e8f7 Add the postcardCollect query and the CollectedPostcard modelPOSTCARD_COLLECT + a CollectedPostcard model (species, medias, mystery/new-species flags, inference mode/type, confidence). Media selects contentUrl via inline MediaImage/MediaVideo fragments.
  4. 2388929 Add collect_postcard with inference-mode gating — Reanalyze (idempotent) then postcardCollect; reanalyzing first is the fix for the postcard internal-server errors. Fixes BB API update / increased number of internal errors from server #29. Related to HA with Blueprint is no longer autocollecting Postcards ha-birdbuddy#98.
  5. edd60fe Remove the legacy sighting/report flow — Delete sighting_from_postcard, finish_postcard, sighting_choose_*, the dead sighting_create / sighting_create_check_progress (their input types are gone upstream), the birdbuddy.sightings module, and the best-guess/anomaly logic. Breaking. Related to Bird Buddy Postcard Event Exceeds 32768 ha-birdbuddy#78 and Modernize packaging, tooling, and CI; refresh the GraphQL schema #40.
  6. 5431c40 Return UNKNOWN for a missing power profile — Drop the bogus "STANDARD" default (never matched STANDARD_MODE); add the schema's ULTRA_FRENZY_MODE.
  7. aae504d Read the owner feeder's nested location — "Owner" feeders nest location{city,country}; read it as well as the flat member/public locationCity/locationCountry.
  8. 31e1505 Paginate collection() to return all media — Follow the connection cursor to completion via a defensive _iter_pages (stops on a stuck cursor); add a page_size guard (1–100, the API's limit). supersedes Paginate collection() to return all media #38.
  9. fb31d54 Paginate and guard the feedrefresh_feed pages to the since cutoff; feed_nodes/new_postcards page fully; feed(first=) guarded to 1–100. Refs BB API update / increased number of internal errors from server #29.
  10. f3c3921 Request createdAt for every feed item typeAnyFeedItem is a union whose members all implement FeedItem; an interface inline fragment makes every member return createdAt. Fixes Some feed items do not have created_at #24.
  11. 84ccbc0 Type BirdBuddy timestamps to match the schemaMedia.created_at / Collection.last_visit are non-null in the schema, so they return datetime; parse_datetime becomes a plain str -> datetime parser and FeedNode.created_at stays optional (the union case).
  12. cb9037b Capture profile and collections, and sanitize dumped payloads — Extend the dumper to capture the ME profile and collections, and add a deterministic sanitizer (UUID remap + PII scrub, enums/species preserved).
  13. a9ab1a9 Regenerate fixtures from real captures — Rebuild api_payloads.json from a fresh sanitized capture (profile, collections, postcards), dropping the dead sighting-flow payloads.
  14. 08104e3 Add client tests for auth, power profile, and firmware — Cover sign-in, token refresh, refresh, collection filtering, feeder options, media sharing, power profile, and the firmware check; the power-profile and firmware responses are real captures of asynchronous mutations.
  15. 82f5689 Document the v0.1.0 breaking changes in the README — A "Breaking changes (v0.0.x → v0.1.0)" section.
  16. 697e5c3 Guard update_firmware_start when no update is available — The API returns an internal error when asked to start an unavailable update; compare the reported versions and raise NoFirmwareUpdateAvailableError instead.
  17. 4d1fd3c Remove the broken latest_collections method — It referenced a query that does not exist and raised AttributeError on every call.
  18. d61eb80 Add graphql-core query validation — A test that validates every query string against the committed schema; test_schema.py only checks input types, so this catches malformed selections/args (it caught a malformed POSTCARD_COLLECT during development).
  19. 92d4031 Show a realistic feeder example in the README — Replace the schema-fragment dump with a runnable example.
  20. d3b3209 Add identify_postcard to read species and media without collecting — Restore the read/preview the removed sighting_from_postcard gave: identify_postcard returns a PostcardAnalysis (recognized species + media + feeder, no collect); the extended POSTCARD_REANALYZE omits reportToken/matchTokens. Only confidently-recognized species are exposed — the "can't decide" suggestions are a manual-identification affordance in the app, not an identification. The dumper now sends the library's own queries so the fixtures can't drift from what the client sends. Related to Bird Buddy Postcard Event Exceeds 32768 ha-birdbuddy#78.
  21. 23f28ff Harden the postcard API and media-expiry against malformed responsesis_media_expired returns None for a URL with no Expires param (as its docstring promises) instead of KeyError; identify_postcard / collect_postcard raise UnexpectedResponseError instead of a bare KeyError/TypeError when the response lacks the expected fields. Both are consumed pervasively by ha-birdbuddy (sensor/image/visitors).

Issue references

  • Fixes Some feed items do not have created_at #24 — feed items with no created_at ('>' not supported between NoneType and datetime); the interface fragment makes the field always present, and a regression test pins the guards.
  • Fixes BB API update / increased number of internal errors from server #29 — postcard internal-server errors; collect_postcard reanalyzes before collecting, and the feed/collection page-size guards stop the first > 100 internal errors.
  • Supersedes and closes Paginate collection() to return all media #38 (@tombeaulah) — the collection-pagination fix, incorporated with a Co-authored-by trailer and extended with the page_size guard. A keyword can't auto-close a PR, so please close Paginate collection() to return all media #38 on merge.
  • Related (ha-birdbuddy, cross-repo — won't auto-close): #98 (autocollect internal errors — fixed for HA users once the integration adopts collect_postcard), #78 (postcard event > 32768 bytes —the bloat was the reportToken/matchTokens, which the new preview omits), #40 (per-photo anomaly correction — obsolete now that the backend aggregates media and reports confidence).

Testing

make check (ruff, ruff format, markdownlint, pyright, pytest) passes locally on Python 3.10.20; CI runs the same gate across 3.10–3.14. Coverage: 71% → 85% (52 → 91 tests). The new test_query_validation.py validates every query against the schema. Beyond the mocked suite, the actual client methods were driven live against a real account: refresh, refresh_collections, collection (cursor pagination), new_postcards, refresh_feed, identify_postcard (a fresh MANUAL_NOT_STARTED postcard → PostcardAnalysis with species +
media), and one opted-in collect_postcard (returned a CollectedPostcard; contentUrl resolved through the fixed query).

y% make check
Running format check, lint, and typecheck...
All checks passed!
31 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               366     98    73%   101, 126-128, 132, 154-155, 168-170, 188-189, 201-204, 259, 264-274, 278, 346, 383-411, 429-459, 499, 505, 527, 625-628, 773-775, 816-818, 850, 872, 884-909, 923-928, 949-958, 997-998
birdbuddy/const.py                  1      0   100%
birdbuddy/exceptions.py            32      1    97%   61
birdbuddy/feed.py                  75      0   100%
birdbuddy/feeder.py               144     11    92%   132, 157, 162, 167, 193-196, 215-216, 225-226, 258
birdbuddy/media.py                 62      1    98%   70
birdbuddy/postcards.py             53      6    89%   29, 49, 54, 59, 68, 109
birdbuddy/queries/__init__.py       2      0   100%
birdbuddy/queries/auth.py           2      0   100%
birdbuddy/queries/birds.py          4      0   100%
birdbuddy/queries/debug.py          1      0   100%
birdbuddy/queries/feeder.py         6      0   100%
birdbuddy/queries/me.py             4      0   100%
birdbuddy/user.py                  11      0   100%
-------------------------------------------------------------
TOTAL                             775    117    85%
Coverage HTML written to dir htmlcov
91 passed in 0.67s
All checks passed.

Follow-up (ha-birdbuddy)

ha-birdbuddy stays pinned ==0.0.20 until a fast-follow PR adopts the new API: identify_postcard (a PostcardAnalysis with species + media) to display the current visitor without collecting, and collect_postcard for the opt-in collect. That migration drops the postcard event bloat (jhansche/ha-birdbuddy#78) and the autocollect errors (#98). I'll prepare it against ha-birdbuddy shortly.

parrot-tailor and others added 30 commits July 7, 2026 12:35
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.
The next release carries breaking changes (the postcard flow is
migrated to postcardCollect and the removed-upstream sighting_create
methods are dropped), so bump the minor version.
Extend scripts/dump_payloads.py to embed the postcardCollect / reanalyze
operation text directly so the script is self-contained, and gate the
destructive postcardCollect behind BB_COLLECT_POSTCARD_ID so it only
runs when a specific postcard is opted in. Reanalyze (AI inference) runs
by default and is idempotent.

Add tests/fixtures/collect_flow.json: a sanitized 2026-07 capture of
reanalyze + postcardCollect against a real account. It pins the flow's
confirmed behavior: reanalyze is synchronous and idempotent
(MANUAL_COMPLETED / ALREADY_REANALYZED), postcardCollect succeeds for an
auto-recognized postcard without mediaSpeciesItems, and
FeedItemCollectedPostcard.species is a list. UUIDs are remapped, media
URLs / feeder identity / tokens scrubbed; enums and public species names
kept.
Add the POSTCARD_COLLECT query string (with a CollectedPostcardFields
fragment) and a CollectedPostcard model wrapping FeedItemCollectedPostcard,
with accessors for species, medias, the mystery/new-species flags,
inference mode/type, and the species-name identification confidence.

Postcard confidence is reported by the API only as enum buckets
(inferenceConfidenceLevel HIGH_CONFIDENCE/LOW_CONFIDENCE at the
new-postcard stage; mediaSpeciesNameIdentificationConfidenceLevel
CANNOT_DECIDE/VERY_CONFIDENT on the postcard) -- the numeric
InferenceMediaRecognitionSuggestion.confidence is not reachable through
the collect flow -- so the model surfaces the enum, not a score.
Add BirdBuddy.collect_postcard, which reanalyzes a postcard first
(idempotent, so it is safe whether or not inference has already run) and
then collects it with the species the backend recognized, returning a
CollectedPostcard. Add a shared _postcard_id resolver used by both
reanalyze and collect.

Reanalyzing before collecting is the fix for the postcard
internal-server errors: converting or collecting a postcard whose
inference had not run errored server-side, and nothing ran inference
first.

Fixes jhansche#29

Related to jhansche/ha-birdbuddy#98
Delete the report-based postcard flow the API and app have moved off of:
the sighting_from_postcard, finish_postcard, sighting_choose_species and
sighting_choose_mystery methods, the removed-upstream sighting_create and
sighting_create_check_progress, their query strings, and the whole
birdbuddy.sightings module (PostcardSighting, SightingReport, Sighting,
SightingType, SightingFinishStrategy, SightingFinishMod,
SightingCreateProgress) with its best-guess/anomaly logic. collect_postcard
replaces it -- a breaking API change.

Drop the now-obsolete tests and the postcard_sighting fixture, and simplify
the schema-drift test since no query references a dropped input type.

Related to jhansche/ha-birdbuddy#78
Related to jhansche/ha-birdbuddy#40
Feeder.power_profile passed a "STANDARD" default into PowerProfile when
the field was absent, which does not match the enum value "STANDARD_MODE"
and so resolved to UNKNOWN while logging a spurious "Unexpected power
profile: STANDARD" warning. Return the reported value, or UNKNOWN when
the feeder does not carry one (non-owner feeders) -- dropping the bogus
default and the warning. A present value (owner responses) is unchanged.
FeederForOwner nests location as location{city,country} while
FeederForMember/Public use flat locationCity/locationCountry;
Feeder.location read only the flat keys, so owner feeders reported
(None, None). Read the nested shape when present, else the flat keys.
Tests cover both shapes and the absent case.
collection() returned only the first page, silently truncating larger
collections. Follow the connection cursor until hasNextPage is false so
the full set of media is returned.

Add a page_size argument (default 50) that caps the per-request batch.
The API rejects a page size above 100 -- it responds HTTP 200 with a
GraphQL INTERNAL_SERVER_ERROR rather than a client error -- so guard the
input to 1-100 and raise ValueError instead of round-tripping to fail.
page_size only tunes how many media are fetched per request; the full
collection is returned regardless.

Paginate through a shared _iter_pages helper that advances by endCursor
and stops defensively when the server claims another page but returns no
advancing cursor (missing, null, or already seen), so a stuck cursor
cannot spin the client in an infinite request loop.

supersedes jhansche#38

Co-authored-by: tombeaulah <tom.beaulah@gmail.com>
The feed methods fetched only the first page. refresh_feed therefore
under-reported when more than one page of items was new since the last
refresh -- fewer results than the app showed -- and new_postcards and
feed_nodes saw only the newest page. feed() also passed first straight
through, so grabbing a large window with first above 100 errored server
side (HTTP 200 with a GraphQL INTERNAL_SERVER_ERROR).

Guard feed(first=) to 1-100 and raise ValueError otherwise. Page
refresh_feed backward through the feed until it reaches items no newer
than since, so every new item is returned rather than truncated; with
no cutoff (no prior refresh) it still returns only the newest page to
avoid replaying the whole history on startup. Page feed_nodes and
new_postcards across every page.

All of it reuses the defensive _iter_pages cursor walk, so a server that
reports another page without an advancing cursor cannot spin the client
in an infinite request loop.

Refs jhansche#29
AnyFeedItem is a union whose members all implement the FeedItem
interface (id, createdAt), but AnyFeedItemFields selected createdAt only
inside inline fragments for the ten enumerated members. Any other member
-- the union has seventeen, and more are added over time -- came back
with just __typename and therefore no createdAt, which surfaced as
sorting and filtering the feed on a null timestamp.

Add an inline fragment on the FeedItem interface so every member,
including those not individually enumerated and any added later, returns
id and createdAt. The None-guards in Feed.newest_edge and Feed.filter
stay as defense in depth.

Fixes jhansche#24
createdAt (Media, FeedItem) and visitLastTime (collections) are
DateTime! in the schema, and every query returning those objects selects
them, so the values are never null. They were typed datetime | None only
because parse_datetime accepted None and propagated it to every caller.

Narrow parse_datetime to str -> datetime and keep optional handling in
FeedNode.created_at alone -- the one caller whose value is genuinely
optional, since feed items are AnyFeedItem union members and a member
whose createdAt the query does not select carries no timestamp.
Media.created_at and Collection.last_visit now return datetime.

Feed.newest_edge filters undated edges with a walrus guard so the type
checker can follow that the compared values are non-null.
Extend scripts/dump_payloads.py to capture the read-only ME profile and
collections alongside the feed and reanalyze, and add a deterministic
sanitizer: UUIDs are remapped with uuid5, and tokens, email, names,
serials, URLs and location are replaced with stable fakes, while enums,
numeric metrics, timestamps and public species names are preserved. The
script writes both the raw dump and a sanitized copy (both git-ignored)
so fixtures can be regenerated cleanly from real responses.
Rebuild tests/fixtures/api_payloads.json from a fresh sanitized capture:
the ME profile (user + feeders), a slice of collections (both bird and
mystery-visitor), and new postcards -- dropping the dead sighting-flow
payloads left over from the removed legacy API. Update test_api_payloads
to the me-wrapped structure and the fresh sanitized values.
Cover the previously untested client operations via graphql_mock and the
regenerated fixtures: sign-in, token refresh, refresh, collection
filtering, feeder options, media sharing, power profile, and the
firmware check. The power-profile and firmware-check responses are real
captures: both mutations resolve asynchronously (power profile reports
the old value until a refresh; an up-to-date firmware check returns a
succeeded result), so the tests exercise that observed behavior.
Add a "Breaking changes (v0.0.x -> v0.1.0)" section covering the move to
collect_postcard, the removed sighting flow and dead sighting_create
methods, collection and feed pagination, the power_profile and location
fixes, and the tightened timestamp return types. Update the intro to
describe collecting postcards rather than finishing sightings.
Starting a firmware update on a feeder already running the latest version
returns an internal server error rather than a client error -- the same
class of misbehavior as the collection and feed page-size limits. The
feeder reports firmwareVersion and availableFirmwareVersion, so compare
them after the progress check and raise NoFirmwareUpdateAvailableError
instead of round-tripping into the server error.
latest_collections referenced queries.me.LATEST_MEDIA, which is not
defined anywhere, so every call raised AttributeError. Remove the dead
public method; refresh_collections covers fetching collections.
test_schema.py only checks that the input types a query references
exist; it cannot catch a malformed field selection or a missing field
argument. Add graphql-core to the dev extra and a test that parses every
query string in birdbuddy.queries and validates it against the committed
introspection schema, so an invalid selection fails make check.
Replace the raw ListFeederFields schema fragment with a runnable example
that logs in and prints each feeder's state, battery, signal, and
firmware version.
Restore the read/preview that the removed sighting_from_postcard gave:
a postcard's recognized species, media, and feeder WITHOUT collecting.
reanalyze_postcard (metadata only) becomes identify_postcard and returns
a PostcardAnalysis.

Extend POSTCARD_REANALYZE to select the sighting-report preview's
confidently-recognized species, the postcard media (inline
MediaImage/MediaVideo contentUrl), and the feeder -- omitting
matchTokens/shareableMatchTokens/reportToken. Only confidently
recognized species are exposed; the can't-decide "suggestions" are a
manual identification affordance in the app, not an identification, so
they are not surfaced.

Have dump_payloads.py send the library's own POSTCARD_REANALYZE and
POSTCARD_COLLECT queries so fixtures cannot drift from what the client
actually sends -- the drift that previously hid query bugs from tests.
Regenerate the collect_flow reanalyze fixture, add PostcardAnalysis
tests, and document the flow in the README.

Related to jhansche/ha-birdbuddy#78
- is_media_expired: return None when the URL has no Expires query param
  (as the docstring already promises) instead of raising KeyError. Real
  signed media URLs carry Expires, so this is defensive.
- identify_postcard / collect_postcard: raise UnexpectedResponseError when
  the GraphQL response lacks the expected nested fields, instead of a bare
  KeyError/TypeError on result[...][...].

Both paths are consumed pervasively by downstream integrations.
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.

BB API update / increased number of internal errors from server Some feed items do not have created_at

1 participant