diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 689855d76..61b81df6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,15 +131,15 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev]" + uv pip install --system -e ".[dev]" -e "./packages/adcp-reporting-storage" - name: Run linter if: matrix.python-version == '3.12' - run: ruff check src/ + run: ruff check src/ packages/adcp-reporting-storage/src/ - name: Run type checker if: matrix.python-version == '3.12' - run: mypy src/adcp/ + run: mypy src/adcp/ packages/adcp-reporting-storage/src/adcp_reporting_storage/ - name: Run adopter type-check suite if: matrix.python-version == '3.12' @@ -245,7 +245,7 @@ jobs: - name: Install dependencies (with [pg] extra) run: | - uv pip install --system -e ".[dev,pg]" + uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run Postgres conformance tests env: @@ -356,8 +356,12 @@ jobs: npm ci npm run build test -f dist/lib/reporting/ledger/index.js + # SDK 16 moved the row storage into the @adcp/reporting-storage workspace + # package, which the root build does not compile; the interop driver reads it. + npm run build --workspace packages/reporting-storage + test -f node_modules/@adcp/reporting-storage/dist/index.js - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run the interop test in both directions env: ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_interop_test @@ -445,7 +449,7 @@ jobs: - name: Install dependencies (with [pg] extra) run: | - uv pip install --system -e ".[dev,pg]" + uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run status semantics, transaction, clock/process and maintenance gates env: @@ -508,7 +512,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run installed frozen binaries and materializer migrations # Propagate pytest failures through tee while retaining evidence for upload. shell: bash @@ -574,7 +578,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run receipt transactions, real mounts, crash and installed gates shell: bash timeout-minutes: 30 @@ -642,7 +646,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Require stopped-worker retention, ownership backfill and receipt isolation env: ADCP_PG_TEST_URL: postgresql://postgres@localhost:5432/adcp_receipt_maintenance_test @@ -690,7 +694,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run frozen feeds, checkpoints, mounts, transaction faults and migrations shell: bash timeout-minutes: 20 @@ -773,7 +777,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Require private feeds and legacy cursor invalidation after maintenance shell: bash timeout-minutes: 45 @@ -836,7 +840,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run installed base and PostgreSQL restart cells shell: bash timeout-minutes: 40 @@ -894,7 +898,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run complete production and projection conformance shell: bash # A passing suite plus harness cleanup can exceed 30 minutes. @@ -977,7 +981,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Run installed source-equivalent production contract shell: bash timeout-minutes: 45 @@ -1045,7 +1049,7 @@ jobs: prune-cache: true - name: Install test dependencies - run: uv pip install --system -e ".[dev,pg]" + run: uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" - name: Require owned production admission and installed maintenance contracts shell: bash timeout-minutes: 42 @@ -1141,6 +1145,10 @@ jobs: python -m build --outdir dist/ python scripts/ci/check_distribution_size.py dist/ pip install dist/*.whl + # The storage distribution is built from its own tree and must + # install over the freshly built adcp wheel, never pull adcp from PyPI. + python -m build --outdir dist-storage/ packages/adcp-reporting-storage + pip install --no-deps dist-storage/*.whl - name: Verify prerelease dependency resolution imports server run: | @@ -1262,7 +1270,15 @@ jobs: ) assert adcp.get_adcp_version(), "ADCP_VERSION file is empty" - print(f"OK — adcp=={adcp.__version__}, spec={adcp.get_adcp_version()}") + # The storage distribution's facade is lazy and its stores work without a driver installed. + import sys + import adcp_reporting_storage + assert not [m for m in sys.modules if m.startswith("adcp_reporting_storage.")] + from adcp_reporting_storage import PgReportingLedgerStore, ReportingRowStorageOptionsV1 + assert "adcp.reporting.ledger.pg" not in sys.modules, "the ledger store moved; adcp must not ship it" + + print(f"OK — adcp=={adcp.__version__}, spec={adcp.get_adcp_version()}, " + f"adcp-reporting-storage=={adcp_reporting_storage.__version__}") PY schema-check: @@ -1291,7 +1307,7 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev]" + uv pip install --system -e ".[dev]" -e "./packages/adcp-reporting-storage" # Blocking. Grades the COMMITTED generated tree against a regeneration # from the COMMITTED schemas, so it runs before the sync steps below @@ -1665,7 +1681,7 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev,pg]" + uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" # Example-local deps: the v3 reference seller imports # sqlalchemy + asyncpg + httpx-respx but those aren't in the # SDK's [dev] extras. Install them inline rather than adding @@ -1740,7 +1756,7 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev,pg]" + uv pip install --system -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]" # Example-local deps: the v3 reference seller imports # sqlalchemy + asyncpg (async Postgres driver) which aren't # in the SDK's [dev,pg] extras. Install inline rather than @@ -2011,7 +2027,7 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev]" + uv pip install --system -e ".[dev]" -e "./packages/adcp-reporting-storage" - name: Map tenant subdomains to localhost run: | @@ -2148,7 +2164,7 @@ jobs: - name: Install dependencies run: | - uv pip install --system -e ".[dev]" + uv pip install --system -e ".[dev]" -e "./packages/adcp-reporting-storage" - name: Boot sales-proposal-mode seller run: | diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5edaca795..5e2a4f2eb 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -45,10 +45,18 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} PROPOSAL_PRS: ${{ steps.release.outputs.prs }} - - name: Queue publication for the created release + - name: Queue publication for the created adcp release if: steps.release.outputs.release_created == 'true' env: GH_TOKEN: ${{ github.token }} RELEASE_SHA: ${{ steps.release.outputs.sha }} RELEASE_TAG: ${{ steps.release.outputs.tag_name }} - run: gh workflow run release-publish.yml --ref main -f release_sha="$RELEASE_SHA" -f tag_name="$RELEASE_TAG" + run: gh workflow run release-publish.yml --ref main -f package=adcp -f release_sha="$RELEASE_SHA" -f tag_name="$RELEASE_TAG" + - name: Queue publication for the created adcp-reporting-storage release + # Manifest mode prefixes every non-root package's outputs with its path. + if: steps.release.outputs['packages/adcp-reporting-storage--release_created'] == 'true' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_SHA: ${{ steps.release.outputs['packages/adcp-reporting-storage--sha'] }} + RELEASE_TAG: ${{ steps.release.outputs['packages/adcp-reporting-storage--tag_name'] }} + run: gh workflow run release-publish.yml --ref main -f package=adcp-reporting-storage -f release_sha="$RELEASE_SHA" -f tag_name="$RELEASE_TAG" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 51fd4a685..22105b728 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -3,6 +3,14 @@ name: Release publish on: workflow_dispatch: inputs: + package: + description: Distribution to publish (each Release Please component publishes on its own) + required: true + type: choice + default: adcp + options: + - adcp + - adcp-reporting-storage release_sha: description: Exact commit of the Release Please release required: true @@ -15,7 +23,7 @@ on: permissions: {} concurrency: - group: release-publish-${{ inputs.tag_name }} + group: release-publish-${{ inputs.package }}-${{ inputs.tag_name }} cancel-in-progress: false jobs: @@ -39,8 +47,15 @@ jobs: GH_TOKEN: ${{ github.token }} RELEASE_SHA: ${{ inputs.release_sha }} RELEASE_TAG: ${{ inputs.tag_name }} + PACKAGE: ${{ inputs.package }} run: | [[ "$RELEASE_SHA" =~ ^[0-9a-f]{40}$ ]] + # adcp tags are vX.Y.Z; the storage component tags carry its name. + case "$PACKAGE" in + adcp) [[ "$RELEASE_TAG" =~ ^v[0-9] ]] ;; + adcp-reporting-storage) [[ "$RELEASE_TAG" =~ ^adcp-reporting-storage-v[0-9] ]] ;; + *) echo "unknown package $PACKAGE" >&2; exit 1 ;; + esac test "$(git rev-parse "$RELEASE_TAG^{commit}")" = "$RELEASE_SHA" git merge-base --is-ancestor "$RELEASE_SHA" HEAD for attempt in {1..220}; do @@ -57,9 +72,15 @@ jobs: with: python-version: '3.12' - name: Build and check distributions + env: + PACKAGE: ${{ inputs.package }} run: | python -m pip install build twine - python -m build + if [ "$PACKAGE" = adcp ]; then + python -m build + else + python -m build --outdir dist/ packages/adcp-reporting-storage + fi python -m twine check dist/* - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: diff --git a/.gitignore b/.gitignore index d913805df..9c13fb12a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ build/ develop-eggs/ dist/ +dist-storage/ # Bundled schema copy — populated by scripts/bundle_schemas.py before # wheel build, mirrors schemas/cache/ so setuptools package-data ships # them. Committed copies would duplicate ~13MB across both trees. diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 30e6f9c78..4ce00a003 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - ".": "9.0.0-beta.4" + ".": "9.0.0-beta.4", + "packages/adcp-reporting-storage": "0.1.0-beta.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f894071c5..354aed72e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ ### ⚠ BREAKING CHANGES +* **reporting:** the Reliable Reporting storage implementations ship as a + separate distribution, `adcp-reporting-storage`, maintained in this + repository and versioned on its own line (`packages/adcp-reporting-storage`). + The PostgreSQL ledger, reconciliation, materializer, receipt, feed, + projection, production, outbox, status, inline-storage and buyer-submission + stores, the chunked row storage with its filesystem, GCS, S3 and Azure + providers, the host change feed, period retirement, row migration, the + caller-ownership migration helpers and `ReliableReportingService.postgres` + moved out of `adcp.reporting`; the old import paths are gone, with no shims. + `adcp` keeps the store protocols, the in-memory stores, the producer, the + handlers, the row encoding and the store-contract errors, and never imports + the new package. The `adcp[reporting-gcs]`, `[reporting-s3]` and + `[reporting-azure]` extras are removed; `adcp[pg]` remains for the signing + and decisioning stores. Install `adcp-reporting-storage[pg]` (plus `gcs`, + `s3`, `azure`) and import from `adcp_reporting_storage`; the full old-to-new + table is in [docs/reporting-storage-package-migration.md](docs/reporting-storage-package-migration.md). + The composition now depends on store protocols + (`adcp.reporting.production.store.ReportingProductionStore`, + `adcp.reporting.outbox.durable.ReportingDurableNotificationLedger`, + `ReportingDurableStatusStore`, `adcp.reporting.ledger.ReportingRowStagingStore`, + `ReportingSourcePublicationStore`, `ReportingWorkerRetryPolicy`) instead of + recognizing the PostgreSQL classes by type; `ReliableReportingService` takes + `staging=`, `seals=` and `initialize_source_storage=`. The public-API + snapshot now covers `adcp.reporting`, `adcp.reporting.ledger` and + `adcp_reporting_storage`. Refs #1510, #1490, #1488, #1489. + * **types:** `ProductFormatDeclaration` is a class again — a `Format` subclass that reads the root rules of `core/product-format-declaration.json` on every validation — rather than an `Annotated` union. Construction, diff --git a/MIGRATION_v8_to_v9.md b/MIGRATION_v8_to_v9.md index 85fd16530..b8b304869 100644 --- a/MIGRATION_v8_to_v9.md +++ b/MIGRATION_v8_to_v9.md @@ -15,6 +15,7 @@ changes below before upgrading each client, server and reporting worker. | `ProductFormatDeclaration` enforces its schema's root rules | It is a `Format` subclass, so construction, `model_validate`, `isinstance` and the projection helpers all work on it, and it refuses a `format_kind` outside the schema's closed set. Read typed parameters with `params_as`. Use `Format` for open consumer parsing of unknown kinds, and `LegacyProductFormatDeclaration` for the generated branch union. A validated declaration is published on `Product.format_options` as-is. `Format` stays the lenient path for reading and publishing stored declarations; `ProductFormatDeclaration` is the authoring check. See [product declarations](docs/types-9-migration.md#product-declarations-are-graded-against-their-root-schema) and [strict authoring and lenient publishing](docs/types-9-migration.md#strict-authoring-lenient-reading-and-publishing). | | Structural pointer references share the selected type | Remove imports of obsolete internal per-position wrappers. Use public `adcp.types` names or `adcp.types.domains` modules. See [pointer references](docs/types-9-migration.md#3-pointer-refs-resolve-to-the-selected-type-1371). | | Format-kind fields preserve open strings | Replace enum identity comparisons with string equality. Opt into `CanonicalFormatKindStr` when your application needs a closed vocabulary. See [format-kind validation](docs/canonical-format-kinds-migration.md). | +| The PostgreSQL reporting stores and row storage providers ship as `adcp-reporting-storage` | Install `adcp-reporting-storage[pg]` (and `gcs`/`s3`/`azure`) and import from `adcp_reporting_storage`; `ReliableReportingService.postgres` is `adcp_reporting_storage.postgres_reliable_reporting_service`. `adcp` keeps the protocols, the in-memory stores and the digest code. See [the storage package migration](docs/reporting-storage-package-migration.md). | | Reporting generations require authenticated caller ownership | Include `consumer_id` in custom store/binding implementations and typed constructors. Migrate populated ledgers with stopped workers; an entirely empty ledger can use the explicit replacement API. See [reporting ownership migration](docs/reporting-caller-ownership-migration.md). | | New source requests and sealed artifacts include their owning consumer | Read `request.identity.consumer_id` when implementing adapters. Historical ownerless artifacts retain their original hashes, but active owned generations refuse ownerless or differently owned frozen acquisitions; reconcile retained state and admit a new generation instead of relabelling evidence. See [reporting ownership migration](docs/reporting-caller-ownership-migration.md). | | URL canonicalization gives an empty path its `/` whatever the query does | The `@target-uri` signature base changes for bare-authority URLs such as `https://seller.example.com`. Dial signed clients at the explicit root path, which canonicalizes identically on both versions, or move an unrespellable agent URL's signer and verifier in one window. Identifier matching on `adagents.json`, `brand.json`, TMP and the governance issuer only becomes more permissive. See [the empty-path canonicalization fix](docs/request-signing-migration.md#6-upgrading-across-the-empty-path-canonicalization-fix). | diff --git a/Makefile b/Makefile index 64bf08d2f..9a8234b35 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,11 @@ help: ## Show this help message @echo 'Available targets:' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' +STORAGE := packages/adcp-reporting-storage +STORAGE_SRC := $(STORAGE)/src/adcp_reporting_storage + install-dev: ## Legacy pip-only install; bootstrap is preferred for contributors - $(PIP) install -e ".[dev]" + $(PIP) install -e ".[dev]" -e "./$(STORAGE)[pg,gcs,s3,azure]" check-uv: @command -v $(UV) >/dev/null || { \ @@ -29,19 +32,19 @@ bootstrap: check-uv ## Recommended contributor setup: uv-managed deps and git ho $(UV) run --extra dev --group dev pre-commit install --hook-type commit-msg format: ## Format code with black (excludes generated files) - $(BLACK) src/ tests/ scripts/ + $(BLACK) src/ tests/ scripts/ $(STORAGE)/src/ @echo "✓ Code formatted successfully (_generated.py excluded via pyproject.toml)" -lint: ## Run linter (ruff) on source code - $(RUFF) check src/ +lint: ## Run linter (ruff) on source code (both distributions) + $(RUFF) check src/ $(STORAGE)/src/ @echo "✓ Linting passed" lint-all: ## Run linter (ruff) on source and tests - $(RUFF) check src/ tests/ + $(RUFF) check src/ $(STORAGE)/src/ tests/ @echo "✓ Source and test linting passed" -typecheck: ## Run type checker (mypy) on source code - $(MYPY) src/adcp/ +typecheck: ## Run type checker (mypy) on source code (both distributions) + $(MYPY) src/adcp/ $(STORAGE_SRC)/ @echo "✓ Type checking passed" typecheck-all: typecheck test-type-checks test-type-checks-pyright check-type-ignore-contract ## Run all type-check contracts @@ -117,6 +120,7 @@ ci-local: lint typecheck-all test validate-generated ## Run core CI checks local clean: ## Clean generated files and caches rm -rf build/ rm -rf dist/ + rm -rf dist-storage/ $(STORAGE)/build/ $(STORAGE)/src/*.egg-info rm -rf *.egg-info rm -rf .pytest_cache/ rm -rf .mypy_cache/ @@ -127,9 +131,10 @@ clean: ## Clean generated files and caches find . -type f -name "*.pyc" -delete @echo "✓ Cleaned all generated files and caches" -build: ## Build distribution packages +build: ## Build distribution packages (adcp into dist/, adcp-reporting-storage into dist-storage/) $(PYTHON) scripts/bundle_schemas.py python -m build + python -m build --outdir dist-storage/ $(STORAGE) @echo "✓ Distribution packages built" # Development workflow commands diff --git a/README.md b/README.md index b6a3a675c..351669a66 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This README serves both sides of an AdCP integration. Jump to what you're doing: - **Connect as a buyer** → [Quick Start: Test Helpers](#quick-start-test-helpers) and [Quick Start: Distributed Operations](#quick-start-distributed-operations). Entry point: `from adcp import ADCPClient, AgentConfig`; start with the `client.simple.*` API. - **Build a seller / agent** → [Building an AdCP Agent](#building-an-adcp-agent). Entry point: `from adcp.server import ADCPHandler, serve`; use the [production seller path](docs/production-seller.md) when adding tenants, durable tasks, and webhooks. -- **Run Reliable Reporting** → [Account currencies](docs/reporting-currency.md), [ledger migrations](docs/reporting-ledger-migration.md), [reconciliation storage](docs/reporting-reconciliation-storage.md), and [durable status notifications](docs/reporting-status-notifications.md). +- **Run Reliable Reporting** → [Account currencies](docs/reporting-currency.md), [ledger migrations](docs/reporting-ledger-migration.md), [reconciliation storage](docs/reporting-reconciliation-storage.md), and [durable status notifications](docs/reporting-status-notifications.md). The PostgreSQL and object-storage stores are the separate [`adcp-reporting-storage`](packages/adcp-reporting-storage/README.md) distribution. - **Understand the type system & imports** → [Type Safety](#type-safety) (import surface, partial modules, cold-start note). - **Test against reference agents** → [Quick Start: Test Helpers](#quick-start-test-helpers) and [Test Helpers](#test-helpers). Entry point: `from adcp.testing import test_agent, creative_agent`. diff --git a/docs/releasing.md b/docs/releasing.md index f3a8ce8d1..5b1a930f1 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -52,11 +52,43 @@ One-time setup: required reviewer and `main` as its deployment branch. - In PyPI → adcp → Publishing, add a Trusted Publisher for owner `adcontextprotocol`, repository `adcp-client-python`, workflow - `release-publish.yml`, and environment `release-publish`. + `release-publish.yml`, and environment `release-publish`. The + `adcp-reporting-storage` project needs the same (see below). - Keep the existing `IPR_APP_ID` and `IPR_APP_PRIVATE_KEY` secrets available to `release-please.yml`. The App needs repository contents and pull-request write access. +## adcp-reporting-storage + +`packages/adcp-reporting-storage` is a second Release Please component +(`separate-pull-requests` is on, so it gets its own release PR) on the same +beta channel. Its tags carry the component name +(`adcp-reporting-storage-v0.1.0-beta.1`); the root keeps bare `vX.Y.Z` tags. +Only commits that touch `packages/adcp-reporting-storage/` bump it, and the +root package lists that path in `exclude-paths` (Release Please otherwise hands +the root every commit), so a protocol fix in `adcp` does not release the storage +package and a storage fix does not release `adcp`. A commit touching both trees +counts for both. Keep storage changes out of a root `Release-As:` commit, or the +storage package takes that version too. Conventional-commit scopes are free text, but +`feat(reporting-storage):` keeps the two changelogs readable. + +On a merge, `release-please.yml` queues `release-publish.yml` with +`package=adcp-reporting-storage`; the workflow builds that package's sdist and +wheel from `packages/adcp-reporting-storage` and publishes them through the same +`release-publish` environment approval. The storage wheel pins +`adcp>=9.0.0bN,<10`; publish the `adcp` release it depends on first. + +One-time setup, before the first storage release: + +- In PyPI, add a **pending** Trusted Publisher for a new project named + `adcp-reporting-storage` (owner `adcontextprotocol`, repository + `adcp-client-python`, workflow `release-publish.yml`, environment + `release-publish`). A pending publisher creates the project on first upload; + without it the publish job fails and the `adcp` release is unaffected. +- The first release PR bumps `0.1.0-beta.0` (the manifest's bootstrap value, + never published) to `0.1.0-beta.1`. Check the PR's `pyproject.toml` version + was normalized to `0.1.0b1` the same way the root is. + If publication fails, fix the cause and rerun `release-publish.yml` for the same release SHA and tag. PyPI will reject files it has already accepted; check its published file list before rerunning a partial upload. Do not create another diff --git a/docs/reliable-reporting-service.md b/docs/reliable-reporting-service.md index a68c9927a..dffee6fd7 100644 --- a/docs/reliable-reporting-service.md +++ b/docs/reliable-reporting-service.md @@ -189,7 +189,7 @@ The PostgreSQL factory installs ledger, source staging, and replay-seal schemas in the caller's pool. Ordinary adapter registrations use those durable stores; restart replay reads the sealed bytes without refetching the source. Open the pool before starting the service and close it after service shutdown settles. -This is a change for existing `postgres()` callers, including those that do not +This is a change for existing PostgreSQL factory callers, including those that do not enable `production`: `initialize()` now bootstraps `reporting_inline_objects` and `reporting_inline_seals` in addition to the ledger schema. Before upgrading, ensure the pool's startup role can run that source-schema DDL. A DDL-restricted @@ -251,16 +251,13 @@ The SDK constructs the component graph: ```python from psycopg_pool import AsyncConnectionPool -from adcp.reporting import ( - ReliableReportingService, - ReportingProductionOptions, - ReportingServiceOffering, -) +from adcp.reporting import ReportingProductionOptions, ReportingServiceOffering +from adcp_reporting_storage import postgres_reliable_reporting_service pool = AsyncConnectionPool(DATABASE_URL, open=False) await pool.open() -reporting = ReliableReportingService.postgres( +reporting = postgres_reliable_reporting_service( pool=pool, account_context=resolve_reporting_context, consumer_status_enabled=True, @@ -323,7 +320,8 @@ contracts, and [the wiring example](../examples/reporting_service_production.py) ## Advanced composition -`postgres()` without production options remains the Core factory. Advanced +`postgres_reliable_reporting_service()` (from `adcp_reporting_storage`) without +production options remains the Core factory. Advanced adopters can still inject workers and receipt handlers, pass explicit `staging=`/`seals=` to `sources.register`, use `constituent_of=` for a custom row identity, or register a complete executor and object reader. Explicit stores are @@ -424,7 +422,8 @@ when its official close becomes due. Every automatic read still waits for its ac offering to be ready. For Core service composition, set `read_jitter_window=timedelta(...)` on -`ReliableReportingService.memory(...)`, `.postgres(...)`, or the constructor. +`ReliableReportingService.memory(...)`, `postgres_reliable_reporting_service(...)`, +or the constructor. For managed production, set the same option on `ReportingProductionOptions`. Direct `ReportingProducer` users can set it on the producer. A nonnegative duration is required; `timedelta(0)` opts out. Managed production owns this diff --git a/docs/reporting-adapter-testing.md b/docs/reporting-adapter-testing.md index 235dfd701..f8714faa5 100644 --- a/docs/reporting-adapter-testing.md +++ b/docs/reporting-adapter-testing.md @@ -31,12 +31,12 @@ Use a request/capability fixture that matches the adapter's declared offering, scope, currency, metrics, and source-local period. The SDK's `redacted_*` fixtures describe a sample provider, not every provider's contract. -To test source replay through PostgreSQL, install `adcp[pg]`, use an isolated +To test source replay through PostgreSQL, install `adcp-reporting-storage[pg]`, use an isolated test database/schema, and inject the public stores. Initialize the schema before wrapping a store; failure wrappers borrow it and do not own DDL or the pool. ```python -from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore +from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore from adcp.reporting.testing import run_reporting_adapter_conformance diff --git a/docs/reporting-caller-ownership-migration.md b/docs/reporting-caller-ownership-migration.md index 09f2f4e37..7f41c7487 100644 --- a/docs/reporting-caller-ownership-migration.md +++ b/docs/reporting-caller-ownership-migration.md @@ -76,7 +76,7 @@ If reporting was enabled but no configuration or other reporting state was ever admitted, use the explicit operator API without creating a quarantine archive: ```python -from adcp.reporting.migration import replace_empty_legacy_reporting +from adcp_reporting_storage.migration import replace_empty_legacy_reporting await replace_empty_legacy_reporting(operator_connection) await store.create_schema() @@ -116,7 +116,7 @@ itself called from async code. import sqlalchemy as sa from alembic import op -from adcp.reporting.migration import ( +from adcp_reporting_storage.migration import ( replace_empty_legacy_reporting_sync, replace_empty_owned_reporting_sync, ) @@ -245,7 +245,7 @@ immutable record identifiers it has already consumed. ## Operator example ```python -from adcp.reporting.migration import ( +from adcp_reporting_storage.migration import ( ReportingOwnershipBackfill, backfill_legacy_reporting, legacy_generation_digest, migrate_legacy_reporting, ) diff --git a/docs/reporting-ledger-migration.md b/docs/reporting-ledger-migration.md index 5bb3c7577..38ca19b7b 100644 --- a/docs/reporting-ledger-migration.md +++ b/docs/reporting-ledger-migration.md @@ -78,17 +78,17 @@ Custom ledger stores must implement `RetryScheduleStore`, or provide a durable store with no shared retry state to avoid silently retrying on every replica. For deployments managed by a migration tool, the standalone migration is -[`reporting_ledger_account_generations.sql`](../src/adcp/reporting/ledger/reporting_ledger_account_generations.sql). +[`reporting_ledger_account_generations.sql`](../packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_account_generations.sql). It upgrades an existing beta.15 ledger by itself, including in autocommit mode. For a combined bootstrap and upgrade, run all five bundled files in one transaction: ```sh psql "$REPORTING_DATABASE_URL" --set=ON_ERROR_STOP=1 --single-transaction \ - -f src/adcp/reporting/ledger/reporting_ledger.sql \ - -f src/adcp/reporting/ledger/reporting_ledger_account_generations.sql \ - -f src/adcp/reporting/ledger/reporting_ledger_obligation_currency.sql \ - -f src/adcp/reporting/ledger/reporting_ledger_reconciliation.sql \ - -f src/adcp/reporting/ledger/reporting_notification_outbox.sql + -f packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger.sql \ + -f packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_account_generations.sql \ + -f packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_obligation_currency.sql \ + -f packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_reconciliation.sql \ + -f packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_notification_outbox.sql ``` Use the ledger's existing `search_path` and a role that owns its tables. All diff --git a/docs/reporting-notification-outbox.md b/docs/reporting-notification-outbox.md index d89581553..17fb3f47f 100644 --- a/docs/reporting-notification-outbox.md +++ b/docs/reporting-notification-outbox.md @@ -30,12 +30,8 @@ existing behavior. A memory outbox shares its ledger's rollback boundary but has no process-crash durability. ```python -from adcp.reporting.ledger import PgReportingReconciliationStore -from adcp.reporting.outbox import ( - PgReportingOutbox, - ReportingEnvelopeCipher, - ReportingNotificationWorker, -) +from adcp.reporting.outbox import ReportingEnvelopeCipher, ReportingNotificationWorker +from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore ledger = PgReportingReconciliationStore(pool=pool, notifications=True) await ledger.create_schema() diff --git a/docs/reporting-row-storage.md b/docs/reporting-row-storage.md index 6725454c3..7c8d3ec48 100644 --- a/docs/reporting-row-storage.md +++ b/docs/reporting-row-storage.md @@ -12,7 +12,7 @@ writes the same tables and the same bytes, so chunks written by either SDK verif ## Enable it ```python -from adcp.reporting.ledger import PgReportingLedgerStore, ReportingRowStorageOptionsV1 +from adcp_reporting_storage import PgReportingLedgerStore, ReportingRowStorageOptionsV1 store = PgReportingLedgerStore( pool=pool, @@ -123,13 +123,13 @@ configuration the allowlist: the database records which binding each revision us can never point reads or writes at a destination the host did not configure. ```python -from adcp.reporting.ledger import ( +from adcp_reporting_storage import ( PgReportingLedgerStore, ReportingObjectRowBindingV1, ReportingPostgresRowBindingV1, ReportingRowStorageOptionsV1, + create_filesystem_reporting_row_object_provider_v1, ) -from adcp.reporting.ledger.row_storage_object import create_filesystem_reporting_row_object_provider_v1 store = PgReportingLedgerStore( pool=pool, @@ -206,19 +206,19 @@ await store.create_schema() ### Cloud providers Cloud providers wrap official clients that the host constructs with its own credentials, so the extras only need to -be installed where those clients are created: `pip install 'adcp[reporting-gcs]'`, `'adcp[reporting-s3]'` or -`'adcp[reporting-azure]'`. A binding's `location` holds only non-secret coordinates, and its optional +be installed where those clients are created: `pip install 'adcp-reporting-storage[gcs]'`, `'adcp-reporting-storage[s3]'` or +`'adcp-reporting-storage[azure]'`. A binding's `location` holds only non-secret coordinates, and its optional `credential_ref` names one of the clients you injected; an unknown reference fails `UNSAFE_BINDING`. Provider errors surface only as stable `ReportingRowStoreError` codes. Row buckets are separate from Managed Delivery buckets. A native version always identifies exactly one write, and every read and delete is pinned to it. An ETag is never enough, because an identical upload re-created at the same key has the same ETag and a delayed delete would remove the replacement. -**Google Cloud Storage** (`adcp.reporting.ledger.row_storage_gcs`): +**Google Cloud Storage** (`adcp_reporting_storage.ledger.row_storage_gcs`): ```python from google.cloud import storage -from adcp.reporting.ledger.row_storage_gcs import create_gcs_reporting_row_object_provider_v1 +from adcp_reporting_storage.ledger.row_storage_gcs import create_gcs_reporting_row_object_provider_v1 gcs = create_gcs_reporting_row_object_provider_v1( storage=storage.Client(), # keyless credentials: workload identity or ADC @@ -236,11 +236,11 @@ gcs = create_gcs_reporting_row_object_provider_v1( retention-driven deletion in the ledger, not in bucket lifecycle. - The client needs `storage.buckets.get` for the probe plus object create, get and delete. -**Amazon S3 and S3-compatible stores** (`adcp.reporting.ledger.row_storage_s3`): +**Amazon S3 and S3-compatible stores** (`adcp_reporting_storage.ledger.row_storage_s3`): ```python import boto3 -from adcp.reporting.ledger.row_storage_s3 import create_s3_reporting_row_object_provider_v1 +from adcp_reporting_storage.ledger.row_storage_s3 import create_s3_reporting_row_object_provider_v1 s3 = create_s3_reporting_row_object_provider_v1(client=boto3.client("s3", region_name="us-east-1")) # ReportingObjectRowBindingV1(provider="s3", location={"bucket": "acme-adcp-rows", "region": "us-east-1"}, ...) @@ -263,12 +263,12 @@ s3 = create_s3_reporting_row_object_provider_v1(client=boto3.client("s3", region - The client needs `s3:ListBucket`, `s3:GetLifecycleConfiguration`, `s3:GetBucketVersioning`, object put and get, `s3:GetObjectVersion` and `s3:DeleteObjectVersion`. `s3:GetBucketPolicyStatus` is optional. -**Azure Blob Storage** (`adcp.reporting.ledger.row_storage_azure`): +**Azure Blob Storage** (`adcp_reporting_storage.ledger.row_storage_azure`): ```python from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient -from adcp.reporting.ledger.row_storage_azure import create_azure_blob_reporting_row_object_provider_v1 +from adcp_reporting_storage.ledger.row_storage_azure import create_azure_blob_reporting_row_object_provider_v1 azure = create_azure_blob_reporting_row_object_provider_v1( client=BlobServiceClient("https://acmerows.blob.core.windows.net", credential=DefaultAzureCredential()) @@ -374,10 +374,13 @@ ADCP_JS_SDK_DIR=/path/to/adcp-client ADCP_PG_TEST_URL=postgresql://... \ pytest tests/conformance/reporting/test_reporting_row_storage_interop_pg.py ``` -It needs Node and a built `adcp-client` checkout (`npm ci && npm run build`); without them it skips. The rows each SDK -wrote are committed under `tests/fixtures/reporting-row-storage-interop-v1/` (refresh them with -`ADCP_UPDATE_INTEROP_FIXTURES=1`), and the replay tests load them into a fresh database and bucket and verify them -here, so Python CI does not need Node. The JavaScript SDK can replay `python-written.json` the same way. +It needs Node and a built `adcp-client` checkout (`npm ci && npm run build && npm run build --workspace +packages/reporting-storage`, since SDK 16 ships the row storage as `@adcp/reporting-storage`). Locally it skips without +them; CI runs it with `ADCP_REQUIRE_JS_INTEROP=1`, so a missing build fails the lane. The rows each SDK wrote are +committed under `tests/fixtures/reporting-row-storage-interop-v1/` (refresh them with +`scripts/regenerate_row_storage_interop_fixtures.py`), and the replay tests load them into a fresh database and bucket +and verify them here, so Python CI does not need Node. A replay test fails, rather than skips, when a fixture is +missing. The JavaScript SDK has no replay of `python-written.json`; the live test is what proves that direction. ## Host change feed and consumers diff --git a/docs/reporting-status-notifications.md b/docs/reporting-status-notifications.md index cdb6555e3..974727c87 100644 --- a/docs/reporting-status-notifications.md +++ b/docs/reporting-status-notifications.md @@ -2,7 +2,7 @@ This caller-ownership release requires all reporting readers, admission and workers to stop during upgrade. Earlier additive or rolling procedures below apply only to pre-ownership releases; use the [maintenance and recovery contract](reporting-caller-ownership-migration.md) for retained state. -Install `adcp[pg]` and mount the optional status lifecycle to publish +Install `adcp-reporting-storage[pg]` and mount the optional status lifecycle to publish `reporting.status_changed`. `get_reporting_status` remains authoritative. C supports Core status; Managed/Reconciled materialization and receipt expiry clocks remain in D. An account with destination bindings cannot advertise C @@ -104,7 +104,7 @@ wire flags. `ReportingLedgerStore` and `ReportingNotificationOutbox` implementations retain their structural contracts. Pure types and memory implementations import without PostgreSQL. PostgreSQL exports load lazily and construction without the extra -raises an actionable `adcp[pg]` installation hint. +raises an actionable `adcp-reporting-storage[pg]` installation hint. `PgStatusNotificationStore.create_schema()` executes the six existing ledger/A/B steps plus `reporting_status_notifications.sql` and diff --git a/docs/reporting-storage-package-migration.md b/docs/reporting-storage-package-migration.md new file mode 100644 index 000000000..a7b55ec06 --- /dev/null +++ b/docs/reporting-storage-package-migration.md @@ -0,0 +1,103 @@ +# Reporting storage moved to `adcp-reporting-storage` + +SDK 9.0.0 ships the Reliable Reporting *storage implementations* as a separate +distribution, `adcp-reporting-storage`, maintained in this repository and +versioned on its own line ([#1510](https://github.com/adcontextprotocol/adcp-client-python/issues/1510)). +`adcp` keeps the store protocols, the in-memory stores, the producer, the +handlers, the row encoding and the digests. The PostgreSQL stores, the chunked +row storage with its filesystem, GCS, S3 and Azure providers, the host change +feed, period retirement, the ownership-migration helpers and the PostgreSQL +service factory moved. + +This is a clean cut, not a deprecation: nothing is on PyPI stable, the 9.0 +betas are the only adopters, and the JavaScript SDK made the same cut during +its 16.0 beta (`@adcp/reporting-storage`). The old import paths no longer +exist, and `adcp` never imports the new package (a test enforces it). + +## Install + +| Before | After | +| --- | --- | +| `pip install "adcp-reporting-storage[pg]"` for the reporting stores | `pip install "adcp-reporting-storage[pg]"` (installs `adcp` too) | +| `adcp-reporting-storage[gcs]`, `adcp-reporting-storage[s3]`, `adcp-reporting-storage[azure]` | `adcp-reporting-storage[gcs]`, `[s3]`, `[azure]` | +| `adcp-reporting-storage[pg]` for `adcp.signing.PgReplayStore` and the decisioning stores | unchanged | + +Pin both: `adcp-reporting-storage` depends on `adcp>=9.0.0bN,<10` and tracks +one `adcp` major. + +## Imports + +Every moved name is importable from the top-level `adcp_reporting_storage` +package, lazily (no driver is imported until a store is constructed). The deep +paths mirror `adcp.reporting`: `adcp.reporting..pg` became +`adcp_reporting_storage..pg`. + +| Old | New | +| --- | --- | +| `adcp.reporting.ledger.PgReportingLedgerStore`, `.ledger.pg` | `adcp_reporting_storage.PgReportingLedgerStore` (`adcp_reporting_storage.ledger.pg`) | +| `adcp.reporting.ledger.PgReportingReconciliationStore`, `.ledger.delivery_pg` | `adcp_reporting_storage.PgReportingReconciliationStore` (`.ledger.delivery_pg`) | +| `adcp.reporting.materializer.PgReportingMaterializerStore` | `adcp_reporting_storage.PgReportingMaterializerStore` (`.materializer.pg`) | +| `adcp.reporting.receipts.PgReportingReceiptStore` | `adcp_reporting_storage.PgReportingReceiptStore` (`.receipts.pg`) | +| `adcp.reporting.feed.PgReportingFeedStore` | `adcp_reporting_storage.PgReportingFeedStore` (`.feed.pg`) | +| `adcp.reporting.projection.PgReportingProjectionStore`, `PgReportingStatusProjection`, `PgReportingProjectionOutbox` | `adcp_reporting_storage.*` (`.projection.pg`, `.projection.notifications`) | +| `adcp.reporting.production.PgReportingProductionStore`, `PgReportingProductionOutbox` | `adcp_reporting_storage.*` (`.production.pg`) | +| `adcp.reporting.outbox.PgReportingOutbox`, `PgReportingStatusOutbox`, `PgStatusNotificationStore`, `PgReportingActivityUnionStore` | `adcp_reporting_storage.*` (`.outbox.pg`, `.outbox.status_pg`, `.outbox.status_activity_pg`) | +| `adcp.reporting.submissions.PgReportingSubmissionIntentStore` | `adcp_reporting_storage.PgReportingSubmissionIntentStore` (`.submissions.pg`) | +| `adcp.reporting.inline_storage.PgReportingStagingStore`, `PgReportingSealStore` | `adcp_reporting_storage.inline_storage.*` (also top-level) | +| `adcp.reporting.migration` (`migrate_legacy_reporting`, `backfill_legacy_reporting`, `replace_empty_legacy_reporting`, `replace_empty_legacy_reporting_sync`, `replace_empty_owned_reporting`, `replace_empty_owned_reporting_sync`, `ReportingOwnershipBackfill`, `ReportingOwnershipMigrationError`, …) | `adcp_reporting_storage.migration` (also top-level) | +| `adcp.reporting.ledger.row_storage*`, `row_write`, `row_migration`, `row_retention`, `period_retirement`, `change_feed` | `adcp_reporting_storage.ledger.` | +| `adcp.reporting.ledger.ReportingRowStorageOptionsV1`, `ReportingObjectRowBindingV1`, `ReportingPostgresRowBindingV1`, `ReportingRowBindingSelectionInputV1`, `ReportingRowIntentSweepV1`, `ReportingRowPruneV1`, `ReportingPeriodRetirementV1`, `ReportingChange*V1`, `ReportingChangeCursorExpiredError` | `adcp_reporting_storage.` | +| `adcp.reporting.ledger.row_storage_object.ReportingRowObjectProviderV1`, `create_filesystem_reporting_row_object_provider_v1`, `run_reporting_row_object_provider_conformance_v1`, `reporting_period_date_v1` | `adcp_reporting_storage.` (`.ledger.row_storage_object`) | +| `adcp.reporting.ledger.row_storage_gcs.create_gcs_reporting_row_object_provider_v1`, `..._s3...`, `..._azure...` | `adcp_reporting_storage.` | +| `adcp.reporting.ledger.status_snapshot.read_snapshot_on`, `settle_snapshot_on`, `persist_replay_lifecycles_on`, `apply_intents_on` | `adcp_reporting_storage.ledger.status_snapshot.` | +| `adcp.reporting.outbox._schema`, `status_schema`; `adcp.reporting..schema` validators and `required_*.json` manifests | `adcp_reporting_storage.outbox._schema`, `.status_schema`; `adcp_reporting_storage..schema` | +| `postgres_reliable_reporting_service(pool=..., ...)` | `adcp_reporting_storage.postgres_reliable_reporting_service(pool=..., ...)` (same keywords) | +| DDL at `adcp/reporting/ledger/*.sql` | `adcp_reporting_storage/ledger/*.sql`; `importlib.resources.files("adcp_reporting_storage.ledger")` | + +What stays in `adcp.reporting.ledger`: `ReportingLedgerStore` and every other +store protocol, `InMemoryReportingLedgerStore`, `ReportingProducer`, +`ReportingStatusHandler`, `ReportingRowStoreError` and +`ReportingRowStoreErrorCode` (store-contract errors every implementation +raises), `ReportingRowPage`, `row_encoding` (the canonical row bytes and +digests), and the `delivery_changes` reconciliation feed port. + +## New protocols in `adcp` + +The protocol layer used to recognize the PostgreSQL classes by `type(...)`. +It now asks the store. If you implement a store, these are additive and +optional; the in-memory stores implement them, and `adcp-reporting-storage` +implements them for PostgreSQL: + +- `adcp.reporting.production.store.ReportingProductionStore` and + `ReportingProductionProjection`: what `ReportingProductionSupport` and + `ReportingProductionOptions` compose over (queues, projection, schema + readiness, delivery windows, maintenance passes). +- `adcp.reporting.outbox.durable.ReportingDurableNotificationLedger` and + `ReportingDurableStatusStore`: how the capability checks prove a durable + notification, activity and status chain. +- `adcp.reporting.ledger.ReportingRowStagingStore`, + `ReportingSourcePublicationStore`, `ReportingWorkerRetryPolicy`: the + optional hooks the producer used to reach through `getattr` + ([#1489](https://github.com/adcontextprotocol/adcp-client-python/issues/1489), + minimal form; the staged-handle variant is a follow-up). +- `ReliableReportingService(...)` takes `staging=`, `seals=` and + `initialize_source_storage=` so a storage distribution can supply durable + source stores without reaching into the service. + +Public attributes replaced private ones where the composition reads them: +`store.production_support` (was `_production_support`), +`store.projection_read_policy` / `projection_read_escalation`, +`store.activate_production(...)` (was `_activate_production`), +`store.source_publication(...)` (was `_source_publication`), +`store.worker_lock_errors()` (was `_worker_lock_errors`), and +`outbox.next_due_account(delivery=..., now=...)`. + +## Tests and tooling + +The conformance tests keep living under `tests/` and import +`adcp_reporting_storage`; CI installs both distributions in every lane +(`uv pip install -e ".[dev,pg]" -e "./packages/adcp-reporting-storage[pg,gcs,s3,azure]"`). +`make lint`, `make typecheck` and `make build` cover both. The public-API +snapshot (`tests/fixtures/public_api_snapshot.json`) now records +`adcp.reporting`, `adcp.reporting.ledger` and `adcp_reporting_storage` +([#1490](https://github.com/adcontextprotocol/adcp-client-python/issues/1490)). diff --git a/docs/reporting-webhook-activity.md b/docs/reporting-webhook-activity.md index 36c93e188..e66a619d9 100644 --- a/docs/reporting-webhook-activity.md +++ b/docs/reporting-webhook-activity.md @@ -13,7 +13,7 @@ are mounted. ## Mounting and lifecycle -Install `adcp[pg]` and use PostgreSQL 16. The +Install `adcp-reporting-storage[pg]` and use PostgreSQL 16. The [fully typed example](../examples/reporting_webhook_activity.py) shows trusted identity resolution, registration persistence, migration, capability validation, both worker phases, HTTP serving, bounded draining, and pool shutdown. diff --git a/examples/reporting_durable_materializer.py b/examples/reporting_durable_materializer.py index ebbf1e0bf..83dd75c17 100644 --- a/examples/reporting_durable_materializer.py +++ b/examples/reporting_durable_materializer.py @@ -10,8 +10,9 @@ import asyncio +from adcp_reporting_storage import PgReportingMaterializerStore + from adcp.reporting.materializer import ( - PgReportingMaterializerStore, ReportingDestinationIO, ReportingDestinationResolver, ReportingDestinationWriter, diff --git a/examples/reporting_production.py b/examples/reporting_production.py index c5ea501a8..5c4728f38 100644 --- a/examples/reporting_production.py +++ b/examples/reporting_production.py @@ -8,6 +8,8 @@ from __future__ import annotations +from adcp_reporting_storage import PgReportingProductionStore, PgReportingStatusProjection + from adcp.decisioning.registry import BuyerAgentRegistry from adcp.reporting.materializer import ( ReportingDestinationIO, @@ -20,7 +22,6 @@ ReportingSubscriptionResolver, ) from adcp.reporting.production import ( - PgReportingProductionStore, ReportingProductionConfigurationTask, ReportingProductionDestination, ReportingProductionOffering, @@ -28,7 +29,6 @@ ReportingProductionSupport, production_notification_workers, ) -from adcp.reporting.projection import PgReportingStatusProjection from adcp.reporting.receipts import ReceiptAccountResolver from adcp.server import serve from adcp.server.auth import BearerTokenAuth, auth_context_factory diff --git a/examples/reporting_receipt_ingress.py b/examples/reporting_receipt_ingress.py index 6320124ce..3e7ff6566 100644 --- a/examples/reporting_receipt_ingress.py +++ b/examples/reporting_receipt_ingress.py @@ -7,13 +7,11 @@ from __future__ import annotations +from adcp_reporting_storage import PgReportingReceiptStore + from adcp.decisioning.registry import BuyerAgentRegistry from adcp.reporting.feed import ReportingFeedStore -from adcp.reporting.receipts import ( - PgReportingReceiptStore, - ReceiptAccountResolver, - ReportingReceiptHandler, -) +from adcp.reporting.receipts import ReceiptAccountResolver, ReportingReceiptHandler from adcp.server import serve from adcp.server.auth import BearerTokenAuth, auth_context_factory diff --git a/examples/reporting_service_production.py b/examples/reporting_service_production.py index 0afde0869..ba8357941 100644 --- a/examples/reporting_service_production.py +++ b/examples/reporting_service_production.py @@ -1,3 +1,5 @@ +from adcp_reporting_storage import postgres_reliable_reporting_service + """Compose PostgreSQL reporting from registered adapters and trusted providers. See docs/reliable-reporting-service.md for ReportingProductionOptions inputs. @@ -33,7 +35,7 @@ def compose_service( settles; configure owned pools with ReportingServiceResource when needed. Reporting admission comes from the typed sync_accounts task, never configure. """ - service = ReliableReportingService.postgres( + service = postgres_reliable_reporting_service( pool=pool, account_context=account_context, production=production ) for name, adapter in adapters.items(): diff --git a/examples/reporting_status_notifications.py b/examples/reporting_status_notifications.py index e5efe2db8..a11b7ec1b 100644 --- a/examples/reporting_status_notifications.py +++ b/examples/reporting_status_notifications.py @@ -1,4 +1,4 @@ -"""Strictly typed #1168C Core mounting; install ``adcp[pg]``. +"""Strictly typed #1168C Core mounting; install ``adcp-reporting-storage[pg]``. The adopter supplies real account configuration and immutable revision-content handlers, trusted caller resolution and a durable subscription/key registry. @@ -13,19 +13,18 @@ from copy import deepcopy from typing import Any, Protocol +from adcp_reporting_storage import PgReportingLedgerStore, PgStatusNotificationStore from psycopg_pool import AsyncConnectionPool from pydantic import BaseModel from adcp.reporting.ledger import ( ConsumerStatusIngest, - PgReportingLedgerStore, ReportingDeliveryEscalation, ReportingStatusCallerResolver, ReportingStatusHandler, ReportingStatusNotificationHandler, ) from adcp.reporting.outbox import ( - PgStatusNotificationStore, ReportingEnvelopeCipher, ReportingNotificationError, ReportingNotificationSubscription, diff --git a/examples/reporting_webhook_activity.py b/examples/reporting_webhook_activity.py index efdaecaa2..49392b77e 100644 --- a/examples/reporting_webhook_activity.py +++ b/examples/reporting_webhook_activity.py @@ -1,4 +1,4 @@ -"""Typed #1168B mounting/lifecycle example; install ``adcp[pg]``. +"""Typed #1168B mounting/lifecycle example; install ``adcp-reporting-storage[pg]``. Call ``register_approved_subscription`` after account authorization and endpoint proof verification. ``run_reporting_service`` accepts the adopter's existing @@ -12,6 +12,7 @@ from collections.abc import Awaitable, Callable, Sequence from typing import Protocol +from adcp_reporting_storage import PgReportingLedgerStore, PgReportingOutbox from psycopg_pool import AsyncConnectionPool from adcp.decisioning import ( @@ -21,9 +22,7 @@ ) from adcp.decisioning.accounts import ResolveContext from adcp.decisioning.handler import PlatformHandler -from adcp.reporting.ledger import PgReportingLedgerStore from adcp.reporting.outbox import ( - PgReportingOutbox, ReportingActivityProjector, ReportingActivitySupport, ReportingEnvelopeCipher, diff --git a/packages/adcp-reporting-storage/CHANGELOG.md b/packages/adcp-reporting-storage/CHANGELOG.md new file mode 100644 index 000000000..15f6f65ba --- /dev/null +++ b/packages/adcp-reporting-storage/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## Unreleased + +### Features + +* **reporting-storage:** first release. The PostgreSQL ledger, reconciliation, + materializer, receipt, feed, projection, production, outbox, status, inline + storage and buyer submission stores, the chunked row storage with its + filesystem, Google Cloud Storage, Amazon S3 and Azure Blob providers, the + host change feed, period retirement, row migration and the caller-ownership + migration helpers, moved here from `adcp.reporting` (adcp-client-python#1510). diff --git a/packages/adcp-reporting-storage/LICENSE b/packages/adcp-reporting-storage/LICENSE new file mode 100644 index 000000000..4aa20f8c0 --- /dev/null +++ b/packages/adcp-reporting-storage/LICENSE @@ -0,0 +1,17 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +Copyright 2025 AdCP Community + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/adcp-reporting-storage/README.md b/packages/adcp-reporting-storage/README.md new file mode 100644 index 000000000..63ede42ae --- /dev/null +++ b/packages/adcp-reporting-storage/README.md @@ -0,0 +1,71 @@ +# adcp-reporting-storage + +PostgreSQL and object-storage implementations of the AdCP Reliable Reporting +stores, maintained in the [adcp-client-python](https://github.com/adcontextprotocol/adcp-client-python) +repository and versioned on its own line. + +[`adcp`](https://pypi.org/project/adcp/) defines the store protocols, the +in-memory stores, the producer, the `get_reporting_status` handlers, the +reconciliation logic and the row encoding. This distribution implements those +protocols durably: + +| What | Where | +| --- | --- | +| Core ledger, reconciliation, materializer, receipt, feed, projection and production stores over PostgreSQL | `adcp_reporting_storage.ledger.pg`, `.ledger.delivery_pg`, `.materializer.pg`, `.receipts.pg`, `.feed.pg`, `.projection.pg`, `.production.pg` | +| Notification, status and activity queues over PostgreSQL | `adcp_reporting_storage.outbox.pg`, `.outbox.status_pg`, `.outbox.status_activity_pg` | +| Chunked revision row storage, with filesystem, Google Cloud Storage, Amazon S3 and Azure Blob providers | `adcp_reporting_storage.ledger.row_storage*` | +| The host change feed, period retirement, row migration and retention | `adcp_reporting_storage.ledger.change_feed`, `.period_retirement`, `.row_migration`, `.row_retention` | +| Durable inline source staging and seals, buyer submission intents | `adcp_reporting_storage.inline_storage`, `.submissions.pg` | +| Caller-ownership migration helpers | `adcp_reporting_storage.migration` | +| The adapter-first `ReliableReportingService` over a borrowed pool | `adcp_reporting_storage.service` | + +Every public name is also importable from the top-level package, lazily: + +```python +from psycopg_pool import AsyncConnectionPool + +from adcp.reporting.ledger import ProducerOfferings, ReportingProducer +from adcp_reporting_storage import PgReportingLedgerStore, ReportingRowStorageOptionsV1 + +async with AsyncConnectionPool(dsn, min_size=2, max_size=10) as pool: + store = PgReportingLedgerStore( + pool=pool, + row_storage=ReportingRowStorageOptionsV1(deployment_namespace="prod-us"), + ) + await store.create_schema() + producer = ReportingProducer(source=..., offerings=ProducerOfferings(...), store=store) + await producer.run_worker() +``` + +## Install + +```bash +pip install "adcp-reporting-storage[pg]" # PostgreSQL stores (psycopg) +pip install "adcp-reporting-storage[pg,gcs]" # + rows on Google Cloud Storage +pip install "adcp-reporting-storage[pg,s3]" # + rows on Amazon S3 +pip install "adcp-reporting-storage[pg,azure]" # + rows on Azure Blob Storage +``` + +`adcp` is a dependency and is installed with it. The cloud providers take a +client you build; the extras only pin the client libraries. + +## Compatibility + +This package implements the protocols of one `adcp` major and imports a few +private helpers of `adcp.reporting` to do so (`adcp.reporting.ledger._record_rows`, +`_delivery_state`, `status_snapshot`). Its dependency range is therefore one +major (`adcp>=9.0.0b4,<10`), and a new `adcp` major means a new release here. +`adcp` never imports this package; a test in the main repository enforces that. + +## Versioning + +0.x: breaking changes may land in a minor release, each with a migration note in +[CHANGELOG.md](CHANGELOG.md). The package reaches 1.0 when the storage layout +shared with the JavaScript SDK (`@adcp/reporting-storage`) is declared stable. + +## Documentation + +The guides live in the repository: [Reliable Reporting service](../../docs/reliable-reporting-service.md), +[row storage](../../docs/reporting-row-storage.md), [ledger migrations](../../docs/reporting-ledger-migration.md), +[caller-ownership migration](../../docs/reporting-caller-ownership-migration.md), +and the [migration note for the 9.0 split](../../docs/reporting-storage-package-migration.md). diff --git a/packages/adcp-reporting-storage/pyproject.toml b/packages/adcp-reporting-storage/pyproject.toml new file mode 100644 index 000000000..a650f5126 --- /dev/null +++ b/packages/adcp-reporting-storage/pyproject.toml @@ -0,0 +1,87 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "adcp-reporting-storage" +version = "0.1.0b0" +description = "PostgreSQL and object-storage implementations of the AdCP Reliable Reporting stores" +authors = [ + {name = "AdCP Community", email = "maintainers@adcontextprotocol.org"} +] +readme = "README.md" +requires-python = ">=3.10" +license = {text = "Apache-2.0"} +keywords = ["adcp", "reporting", "postgresql", "advertising"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +dependencies = [ + # The store protocols, in-memory stores, producer, handlers, row encoding + # and record codecs this distribution implements against. The stores in + # this package subclass nothing in ``adcp`` but import its private + # ``adcp.reporting`` helpers, so the range is one major: a new ``adcp`` + # major means a new release here. + "adcp>=9.0.0b4,<10", + # RFC 8785 canonical JSON for row bodies and object keys; the same pin as adcp. + "rfc8785>=0.1.4", +] + +[project.optional-dependencies] +pg = [ + # Every store in this package is PostgreSQL-backed. psycopg3 ships both + # sync and async pool interfaces; the stores take an AsyncConnectionPool. + "psycopg[binary]>=3.1.0", + "psycopg-pool>=3.2.0", +] +gcs = [ + # Row storage on Google Cloud Storage. The provider takes a client you build, + # with keyless credentials; it never imports the library itself. + "google-cloud-storage>=2.18", +] +s3 = [ + # Row storage on Amazon S3 or an S3-compatible store (needs bucket versioning). + "boto3>=1.35", +] +azure = [ + # Row storage on Azure Blob Storage (needs blob versioning on the account). + "azure-storage-blob>=12.20", +] + +[project.urls] +Homepage = "https://github.com/adcontextprotocol/adcp-client-python" +Documentation = "https://github.com/adcontextprotocol/adcp-client-python/blob/main/packages/adcp-reporting-storage/README.md" +Repository = "https://github.com/adcontextprotocol/adcp-client-python" +Issues = "https://github.com/adcontextprotocol/adcp-client-python/issues" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +adcp_reporting_storage = [ + "py.typed", + "ledger/*.sql", + "ledger/*.json", + "outbox/*.json", + "materializer/*.json", + "receipts/*.json", + "feed/*.json", + "projection/*.json", + "production/*.json", + "production/*.sql", +] + +# Inside this repository the dependency is the checkout, not PyPI. uv ignores +# this when the package is installed from a built distribution. +[tool.uv.sources] +adcp = { workspace = true } diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/__init__.py new file mode 100644 index 000000000..8979c0afc --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/__init__.py @@ -0,0 +1,353 @@ +"""PostgreSQL and object-storage implementations of the AdCP Reliable Reporting stores. + +``adcp`` defines the store protocols, the in-memory stores, the producer, the +handlers and the row encoding. This distribution implements those protocols +over PostgreSQL and, for revision rows, over a filesystem or an object store +(Google Cloud Storage, Amazon S3, Azure Blob Storage). It is versioned on its +own line so a storage fix never has to ship as a protocol-client release. + +What you assemble:: + + from psycopg_pool import AsyncConnectionPool + from adcp.reporting.ledger import ProducerOfferings, ReportingProducer + from adcp_reporting_storage import PgReportingLedgerStore + + store = PgReportingLedgerStore(pool=pool) + await store.create_schema() + producer = ReportingProducer(source=..., offerings=..., store=store) + +or, for the adapter-first service:: + + from adcp_reporting_storage import postgres_reliable_reporting_service + + reporting = postgres_reliable_reporting_service(pool=pool, account_context=...) + +Names are resolved lazily (:pep:`562`): importing this package loads no driver +and no cloud SDK until the first store or provider is touched. The layout +mirrors ``adcp.reporting`` (``adcp_reporting_storage.ledger.pg`` is where +``PgReportingLedgerStore`` is defined), so a deep import is a supported +address too. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + __version__: str + from adcp_reporting_storage.feed.pg import PgReportingFeedStore as PgReportingFeedStore + from adcp_reporting_storage.inline_storage import ( + PgReportingSealStore as PgReportingSealStore, + ) + from adcp_reporting_storage.inline_storage import ( + PgReportingStagingStore as PgReportingStagingStore, + ) + from adcp_reporting_storage.ledger.change_feed import ( + ReportingChangeCursorExpiredError as ReportingChangeCursorExpiredError, + ) + from adcp_reporting_storage.ledger.change_feed import ( + ReportingChangeKindV1 as ReportingChangeKindV1, + ) + from adcp_reporting_storage.ledger.change_feed import ( + ReportingChangeRecordV1 as ReportingChangeRecordV1, + ) + from adcp_reporting_storage.ledger.change_feed import ( + ReportingChangesPageV1 as ReportingChangesPageV1, + ) + from adcp_reporting_storage.ledger.delivery_pg import ( + PgReportingReconciliationStore as PgReportingReconciliationStore, + ) + from adcp_reporting_storage.ledger.period_retirement import ( + ReportingPeriodRetirementV1 as ReportingPeriodRetirementV1, + ) + from adcp_reporting_storage.ledger.pg import ( + PgReportingLedgerStore as PgReportingLedgerStore, + ) + from adcp_reporting_storage.ledger.row_migration import ( + ReportingRowMigrationBatchV1 as ReportingRowMigrationBatchV1, + ) + from adcp_reporting_storage.ledger.row_migration import ( + ReportingRowQuarantineReason as ReportingRowQuarantineReason, + ) + from adcp_reporting_storage.ledger.row_retention import ( + ReportingRowIntentSweepV1 as ReportingRowIntentSweepV1, + ) + from adcp_reporting_storage.ledger.row_retention import ( + ReportingRowPruneV1 as ReportingRowPruneV1, + ) + from adcp_reporting_storage.ledger.row_storage import ( + ReportingObjectRowBindingV1 as ReportingObjectRowBindingV1, + ) + from adcp_reporting_storage.ledger.row_storage import ( + ReportingPostgresRowBindingV1 as ReportingPostgresRowBindingV1, + ) + from adcp_reporting_storage.ledger.row_storage import ( + ReportingRowBindingSelectionInputV1 as ReportingRowBindingSelectionInputV1, + ) + from adcp_reporting_storage.ledger.row_storage import ( + ReportingRowStorageOptionsV1 as ReportingRowStorageOptionsV1, + ) + from adcp_reporting_storage.ledger.row_storage import ( + ReportingRowStorageV1 as ReportingRowStorageV1, + ) + from adcp_reporting_storage.ledger.row_storage_azure import ( + create_azure_blob_reporting_row_object_provider_v1 as create_azure_blob_reporting_row_object_provider_v1, # noqa: E501 + ) + from adcp_reporting_storage.ledger.row_storage_gcs import ( + assert_gcs_reporting_row_bucket_policy_v1 as assert_gcs_reporting_row_bucket_policy_v1, + ) + from adcp_reporting_storage.ledger.row_storage_gcs import ( + create_gcs_reporting_row_object_provider_v1 as create_gcs_reporting_row_object_provider_v1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectContextV1 as ReportingRowObjectContextV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectDeleteInputV1 as ReportingRowObjectDeleteInputV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectGetInputV1 as ReportingRowObjectGetInputV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectProviderV1 as ReportingRowObjectProviderV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectPutInputV1 as ReportingRowObjectPutInputV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectPutResultV1 as ReportingRowObjectPutResultV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectStatInputV1 as ReportingRowObjectStatInputV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectStatResultV1 as ReportingRowObjectStatResultV1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + create_filesystem_reporting_row_object_provider_v1 as create_filesystem_reporting_row_object_provider_v1, # noqa: E501 + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + render_reporting_row_object_key_v1 as render_reporting_row_object_key_v1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + reporting_period_date_v1 as reporting_period_date_v1, + ) + from adcp_reporting_storage.ledger.row_storage_object import ( + run_reporting_row_object_provider_conformance_v1 as run_reporting_row_object_provider_conformance_v1, # noqa: E501 + ) + from adcp_reporting_storage.ledger.row_storage_s3 import ( + create_s3_reporting_row_object_provider_v1 as create_s3_reporting_row_object_provider_v1, + ) + from adcp_reporting_storage.materializer.pg import ( + PgReportingMaterializerStore as PgReportingMaterializerStore, + ) + from adcp_reporting_storage.migration import ( + ReportingOwnershipBackfill as ReportingOwnershipBackfill, + ) + from adcp_reporting_storage.migration import ( + ReportingOwnershipMigrationError as ReportingOwnershipMigrationError, + ) + from adcp_reporting_storage.migration import ( + backfill_legacy_reporting as backfill_legacy_reporting, + ) + from adcp_reporting_storage.migration import ( + legacy_generation_digest as legacy_generation_digest, + ) + from adcp_reporting_storage.migration import ( + migrate_legacy_reporting as migrate_legacy_reporting, + ) + from adcp_reporting_storage.migration import ( + replace_empty_legacy_reporting as replace_empty_legacy_reporting, + ) + from adcp_reporting_storage.migration import ( + replace_empty_legacy_reporting_sync as replace_empty_legacy_reporting_sync, + ) + from adcp_reporting_storage.migration import ( + replace_empty_owned_reporting as replace_empty_owned_reporting, + ) + from adcp_reporting_storage.migration import ( + replace_empty_owned_reporting_sync as replace_empty_owned_reporting_sync, + ) + from adcp_reporting_storage.migration import ( + require_owned_schema_or_empty as require_owned_schema_or_empty, + ) + from adcp_reporting_storage.outbox.pg import PgReportingOutbox as PgReportingOutbox + from adcp_reporting_storage.outbox.status_activity_pg import ( + PgReportingActivityUnionStore as PgReportingActivityUnionStore, + ) + from adcp_reporting_storage.outbox.status_pg import ( + PgReportingStatusOutbox as PgReportingStatusOutbox, + ) + from adcp_reporting_storage.outbox.status_pg import ( + PgStatusNotificationStore as PgStatusNotificationStore, + ) + from adcp_reporting_storage.production.pg import ( + PgReportingProductionOutbox as PgReportingProductionOutbox, + ) + from adcp_reporting_storage.production.pg import ( + PgReportingProductionStore as PgReportingProductionStore, + ) + from adcp_reporting_storage.projection.notifications import ( + PgReportingProjectionOutbox as PgReportingProjectionOutbox, + ) + from adcp_reporting_storage.projection.pg import ( + PgReportingProjectionStore as PgReportingProjectionStore, + ) + from adcp_reporting_storage.projection.pg import ( + PgReportingStatusProjection as PgReportingStatusProjection, + ) + from adcp_reporting_storage.receipts.pg import ( + PgReportingReceiptStore as PgReportingReceiptStore, + ) + from adcp_reporting_storage.service import ( + postgres_reliable_reporting_service as postgres_reliable_reporting_service, + ) + from adcp_reporting_storage.submissions.pg import ( + PgReportingSubmissionIntentStore as PgReportingSubmissionIntentStore, + ) + +#: Public name -> (module under this package, attribute). Resolved on first use. +_LAZY_EXPORTS: dict[str, tuple[str, str]] = { + # Stores + "PgReportingLedgerStore": ("ledger.pg", "PgReportingLedgerStore"), + "PgReportingReconciliationStore": ("ledger.delivery_pg", "PgReportingReconciliationStore"), + "PgReportingMaterializerStore": ("materializer.pg", "PgReportingMaterializerStore"), + "PgReportingReceiptStore": ("receipts.pg", "PgReportingReceiptStore"), + "PgReportingFeedStore": ("feed.pg", "PgReportingFeedStore"), + "PgReportingProjectionStore": ("projection.pg", "PgReportingProjectionStore"), + "PgReportingStatusProjection": ("projection.pg", "PgReportingStatusProjection"), + "PgReportingProjectionOutbox": ("projection.notifications", "PgReportingProjectionOutbox"), + "PgReportingProductionStore": ("production.pg", "PgReportingProductionStore"), + "PgReportingProductionOutbox": ("production.pg", "PgReportingProductionOutbox"), + "PgReportingOutbox": ("outbox.pg", "PgReportingOutbox"), + "PgReportingStatusOutbox": ("outbox.status_pg", "PgReportingStatusOutbox"), + "PgStatusNotificationStore": ("outbox.status_pg", "PgStatusNotificationStore"), + "PgReportingActivityUnionStore": ("outbox.status_activity_pg", "PgReportingActivityUnionStore"), + "PgReportingSubmissionIntentStore": ("submissions.pg", "PgReportingSubmissionIntentStore"), + "PgReportingStagingStore": ("inline_storage", "PgReportingStagingStore"), + "PgReportingSealStore": ("inline_storage", "PgReportingSealStore"), + # The adapter-first service over PostgreSQL + "postgres_reliable_reporting_service": ("service", "postgres_reliable_reporting_service"), + # Caller-ownership migration + "ReportingOwnershipBackfill": ("migration", "ReportingOwnershipBackfill"), + "ReportingOwnershipMigrationError": ("migration", "ReportingOwnershipMigrationError"), + "backfill_legacy_reporting": ("migration", "backfill_legacy_reporting"), + "legacy_generation_digest": ("migration", "legacy_generation_digest"), + "migrate_legacy_reporting": ("migration", "migrate_legacy_reporting"), + "replace_empty_legacy_reporting": ("migration", "replace_empty_legacy_reporting"), + "replace_empty_legacy_reporting_sync": ("migration", "replace_empty_legacy_reporting_sync"), + "replace_empty_owned_reporting": ("migration", "replace_empty_owned_reporting"), + "replace_empty_owned_reporting_sync": ("migration", "replace_empty_owned_reporting_sync"), + "require_owned_schema_or_empty": ("migration", "require_owned_schema_or_empty"), + # Host change feed + "ReportingChangeCursorExpiredError": ( + "ledger.change_feed", + "ReportingChangeCursorExpiredError", + ), + "ReportingChangeKindV1": ("ledger.change_feed", "ReportingChangeKindV1"), + "ReportingChangeRecordV1": ("ledger.change_feed", "ReportingChangeRecordV1"), + "ReportingChangesPageV1": ("ledger.change_feed", "ReportingChangesPageV1"), + # Row storage: options, bindings, maintenance results + "ReportingRowStorageV1": ("ledger.row_storage", "ReportingRowStorageV1"), + "ReportingRowStorageOptionsV1": ("ledger.row_storage", "ReportingRowStorageOptionsV1"), + "ReportingObjectRowBindingV1": ("ledger.row_storage", "ReportingObjectRowBindingV1"), + "ReportingPostgresRowBindingV1": ("ledger.row_storage", "ReportingPostgresRowBindingV1"), + "ReportingRowBindingSelectionInputV1": ( + "ledger.row_storage", + "ReportingRowBindingSelectionInputV1", + ), + "ReportingRowIntentSweepV1": ("ledger.row_retention", "ReportingRowIntentSweepV1"), + "ReportingRowPruneV1": ("ledger.row_retention", "ReportingRowPruneV1"), + "ReportingPeriodRetirementV1": ("ledger.period_retirement", "ReportingPeriodRetirementV1"), + "ReportingRowMigrationBatchV1": ("ledger.row_migration", "ReportingRowMigrationBatchV1"), + "ReportingRowQuarantineReason": ("ledger.row_migration", "ReportingRowQuarantineReason"), + # Row object providers: the contract, the filesystem reference, the cloud factories + "ReportingRowObjectProviderV1": ("ledger.row_storage_object", "ReportingRowObjectProviderV1"), + "ReportingRowObjectContextV1": ("ledger.row_storage_object", "ReportingRowObjectContextV1"), + "ReportingRowObjectPutInputV1": ("ledger.row_storage_object", "ReportingRowObjectPutInputV1"), + "ReportingRowObjectPutResultV1": ("ledger.row_storage_object", "ReportingRowObjectPutResultV1"), + "ReportingRowObjectGetInputV1": ("ledger.row_storage_object", "ReportingRowObjectGetInputV1"), + "ReportingRowObjectDeleteInputV1": ( + "ledger.row_storage_object", + "ReportingRowObjectDeleteInputV1", + ), + "ReportingRowObjectStatInputV1": ("ledger.row_storage_object", "ReportingRowObjectStatInputV1"), + "ReportingRowObjectStatResultV1": ( + "ledger.row_storage_object", + "ReportingRowObjectStatResultV1", + ), + "render_reporting_row_object_key_v1": ( + "ledger.row_storage_object", + "render_reporting_row_object_key_v1", + ), + "reporting_period_date_v1": ("ledger.row_storage_object", "reporting_period_date_v1"), + "create_filesystem_reporting_row_object_provider_v1": ( + "ledger.row_storage_object", + "create_filesystem_reporting_row_object_provider_v1", + ), + "run_reporting_row_object_provider_conformance_v1": ( + "ledger.row_storage_object", + "run_reporting_row_object_provider_conformance_v1", + ), + "create_gcs_reporting_row_object_provider_v1": ( + "ledger.row_storage_gcs", + "create_gcs_reporting_row_object_provider_v1", + ), + "assert_gcs_reporting_row_bucket_policy_v1": ( + "ledger.row_storage_gcs", + "assert_gcs_reporting_row_bucket_policy_v1", + ), + "create_s3_reporting_row_object_provider_v1": ( + "ledger.row_storage_s3", + "create_s3_reporting_row_object_provider_v1", + ), + "create_azure_blob_reporting_row_object_provider_v1": ( + "ledger.row_storage_azure", + "create_azure_blob_reporting_row_object_provider_v1", + ), +} + +_LAZY_SUBMODULES = frozenset( + { + "feed", + "inline_storage", + "ledger", + "materializer", + "migration", + "outbox", + "production", + "projection", + "receipts", + "service", + "submissions", + } +) + +__all__ = sorted(_LAZY_EXPORTS) + + +def __getattr__(name: str) -> Any: + if name == "__version__": + # Read the installed distribution's PEP 440 version rather than a + # literal Release Please would rewrite in its own -beta.N spelling. + from importlib.metadata import PackageNotFoundError, version + + try: + return version("adcp-reporting-storage") + except PackageNotFoundError: + return "0.0.0" + lazy_export = _LAZY_EXPORTS.get(name) + if lazy_export is not None: + import importlib + + module_name, attribute = lazy_export + return getattr(importlib.import_module(f"{__name__}.{module_name}"), attribute) + if name in _LAZY_SUBMODULES: + import importlib + + return importlib.import_module(f"{__name__}.{name}") + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__() -> list[str]: + return sorted(set(__all__) | _LAZY_SUBMODULES | {"__version__"}) diff --git a/src/adcp/reporting/_inline_storage_schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/_inline_storage_schema.py similarity index 100% rename from src/adcp/reporting/_inline_storage_schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/_inline_storage_schema.py diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/__init__.py new file mode 100644 index 000000000..6c6b9c1c1 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL frozen feed store.""" diff --git a/src/adcp/reporting/feed/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/pg.py similarity index 96% rename from src/adcp/reporting/feed/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/pg.py index 867246426..9a99288bc 100644 --- a/src/adcp/reporting/feed/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/pg.py @@ -16,7 +16,6 @@ from adcp.reporting.feed.errors import ReportingFeedError from adcp.reporting.feed.projection import capture_feed from adcp.reporting.feed.request import FeedRequest -from adcp.reporting.feed.schema import validate_feed_schema from adcp.reporting.feed.snapshot import ( ReportingFeedSnapshot, StoredFeedSnapshot, @@ -25,14 +24,15 @@ ) from adcp.reporting.ledger.delivery_changes import ReportingReconciliationChange from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal -from adcp.reporting.ledger.pg import _BOUND_CONNECTION from adcp.reporting.ledger.status_projection import ReportingStatusSnapshot -from adcp.reporting.ledger.status_snapshot import read_snapshot_on from adcp.reporting.materializer.capture import decode_materializer_boundary -from adcp.reporting.materializer.pg import _now from adcp.reporting.receipts.capture import decode_receipt_boundary -from adcp.reporting.receipts.pg import PgReportingReceiptStore from adcp.server.helpers import inject_context +from adcp_reporting_storage.feed.schema import validate_feed_schema +from adcp_reporting_storage.ledger.pg import _BOUND_CONNECTION +from adcp_reporting_storage.ledger.status_snapshot import read_snapshot_on +from adcp_reporting_storage.materializer.pg import _now +from adcp_reporting_storage.receipts.pg import PgReportingReceiptStore @dataclass(frozen=True, repr=False) @@ -95,7 +95,7 @@ class PgReportingFeedStore(PgReportingReceiptStore): async def create_schema(self) -> None: async with self._connection() as connection, connection.transaction(): await self._create_schema_on(connection) - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") for name in ( "reporting_materializer.sql", "reporting_receipt_ingestion.sql", diff --git a/src/adcp/reporting/feed/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/required_schema.json similarity index 100% rename from src/adcp/reporting/feed/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/required_schema.json diff --git a/src/adcp/reporting/feed/schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/schema.py similarity index 76% rename from src/adcp/reporting/feed/schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/schema.py index 0a0e74823..cddf4eb20 100644 --- a/src/adcp/reporting/feed/schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/feed/schema.py @@ -7,15 +7,15 @@ from typing import Any from adcp.reporting.feed.errors import ReportingFeedError -from adcp.reporting.outbox._schema import schema_objects -from adcp.reporting.receipts.schema import validate_receipt_schema +from adcp_reporting_storage.outbox._schema import schema_objects +from adcp_reporting_storage.receipts.schema import validate_receipt_schema async def validate_feed_schema(connection: Any, *, notifications: bool = False) -> None: ready = False try: required = json.loads( - files("adcp.reporting.feed").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.feed").joinpath("required_schema.json").read_text() ) actual = await schema_objects(connection) if required and all(actual.get(k) == v for k, v in required.items()): diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/inline_storage.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/inline_storage.py new file mode 100644 index 000000000..539b28106 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/inline_storage.py @@ -0,0 +1,304 @@ +"""PostgreSQL inline source storage: immutable staged objects and seals. + +The staging and seal stores keep the producer's sealed source slices in the +same database as the ledger, under the ledger's account lock when the +PostgreSQL ledger store publishes a seal. The contract, the prepared-object +shapes and the error policy are :mod:`adcp.reporting.inline_storage`; this +module is the durable backend. +""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncIterator, Mapping +from contextlib import asynccontextmanager +from importlib.resources import files +from typing import TYPE_CHECKING, Any, ClassVar + +from adcp.reporting.inline_source import SealedSlice +from adcp.reporting.inline_storage import ( + _DIGEST, + INLINE_STAGING_MAX_BYTES, + InlineStorageError, + PreparedBackendObjectV1, + PreparedBackendSealV1, + _account, + _identity, + _object_inputs, + _payload_digest, + _prepared_identity, + _redact, + _reference, + _seal, +) +from adcp.reporting.source import SOURCE_BATCH_MANIFEST_MAX_BYTES_V1, SourceBatchManifestReferenceV1 +from adcp_reporting_storage._inline_storage_schema import REQUIRED_OBJECTS +from adcp_reporting_storage.outbox._schema import schema_objects + +if TYPE_CHECKING: + from psycopg_pool import AsyncConnectionPool + +__all__ = ["PgReportingSealStore", "PgReportingStagingStore"] + + +class _PgStorage: + is_durable: ClassVar[bool] = True + + def __init__(self, *, pool: AsyncConnectionPool) -> None: + try: + from psycopg_pool import AsyncConnectionPool as Pool + except ImportError: + pass + else: + if not isinstance(pool, Pool): + raise InlineStorageError("INVALID_INPUT") + self._pool = pool + return + raise ImportError("PostgreSQL inline storage requires adcp-reporting-storage[pg]") + + def __repr__(self) -> str: + return f"{type(self).__name__}()" + + async def _ready_on(self, connection: Any) -> None: + installed = await schema_objects(connection) + if not REQUIRED_OBJECTS or any( + installed.get(key, {}).get(field) != value[field] + for key, value in REQUIRED_OBJECTS.items() + for field in ("fingerprint", "enabled") + ): + raise InlineStorageError("SCHEMA_UNREADY") + + @asynccontextmanager + async def _transaction(self) -> AsyncIterator[Any]: + from psycopg.errors import UndefinedTable + + async with self._pool.connection() as connection, connection.transaction(): + # DO NOTHING's conflict winner must be visible to the next SELECT, + # regardless of a pool's default isolation level. + await connection.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED") + try: + await connection.execute( + "LOCK TABLE reporting_inline_objects, reporting_inline_seals " + "IN ACCESS SHARE MODE" + ) + except UndefinedTable: + raise InlineStorageError("SCHEMA_UNREADY") from None + await self._ready_on(connection) + yield connection + + @_redact + async def create_schema(self) -> None: + """Install the standalone additive migration and verify it atomically.""" + async with self._pool.connection() as connection, connection.transaction(): + await connection.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED") + await connection.execute( + files("adcp_reporting_storage.ledger") + .joinpath("reporting_inline_storage.sql") + .read_text() + ) + await self._ready_on(connection) + + @_redact + async def check_ready(self) -> None: + """Verify required schema objects, including constraints and write guards.""" + async with self._pool.connection() as connection, connection.transaction(): + await self._ready_on(connection) + + +class PgReportingStagingStore(_PgStorage): + """Immutable account-qualified content storage using a borrowed async pool.""" + + def __init__( + self, *, pool: AsyncConnectionPool, max_payload_bytes: int = INLINE_STAGING_MAX_BYTES + ) -> None: + if ( + type(max_payload_bytes) is not int + or not 1 <= max_payload_bytes <= INLINE_STAGING_MAX_BYTES + ): + raise InlineStorageError("INVALID_INPUT") + super().__init__(pool=pool) + self._max_payload_bytes = max_payload_bytes + + async def _read_on(self, connection: Any, account_id: str, digest: str) -> bytes: + row = await ( + await connection.execute( + "SELECT CASE WHEN octet_length(payload) <= %s THEN payload END" + " FROM reporting_inline_objects WHERE account_id = %s AND payload_sha256 = %s", + (self._max_payload_bytes, account_id, digest), + ) + ).fetchone() + if row is None: + raise InlineStorageError("NOT_FOUND") + payload = row[0] + if type(payload) is not bytes or await _payload_digest(payload) != digest: + raise InlineStorageError("INTEGRITY_FAILED") + return payload + + async def _prepare_object( + self, *, account_id: str, source_execution_key: str, ordinal: int, payload: bytes + ) -> PreparedBackendObjectV1: + _object_inputs(account_id, source_execution_key, ordinal, payload, self._max_payload_bytes) + digest = await _payload_digest(payload) + return PreparedBackendObjectV1(account_id, source_execution_key, ordinal, payload, digest) + + async def _stage_on( + self, connection: Any, prepared: PreparedBackendObjectV1 + ) -> tuple[str, str]: + """Stage on the caller's audited transaction; the returned identity is tentative. + + Caller owns account-lock ordering and the full transaction/error boundary. + No checkout, task, transaction, commit, drain or fence is owned here. + """ + if type(prepared) is not PreparedBackendObjectV1: + raise InlineStorageError("INVALID_INPUT") + prepared._validate(self._max_payload_bytes) + await connection.execute( + "INSERT INTO reporting_inline_objects (account_id, payload_sha256, payload)" + " VALUES (%s, %s, %s) ON CONFLICT (account_id, payload_sha256) DO NOTHING", + (prepared.account_id, prepared.payload_sha256, prepared.payload), + ) + if ( + await self._read_on(connection, prepared.account_id, prepared.payload_sha256) + != prepared.payload + ): + raise InlineStorageError("INTEGRITY_FAILED") + return _reference(prepared.account_id, prepared.payload_sha256), prepared.payload_sha256 + + @_redact + async def stage( + self, *, account_id: str, source_execution_key: str, ordinal: int, payload: bytes + ) -> tuple[str, str]: + prepared = await self._prepare_object( + account_id=account_id, + source_execution_key=source_execution_key, + ordinal=ordinal, + payload=payload, + ) + async with self._transaction() as connection: + return await self._stage_on(connection, prepared) + + @_redact + async def read( + self, + *, + object_ref: str, + object_generation: str, + account_id: str, + source_scope: Mapping[str, Any], + cancel: asyncio.Event, + ) -> bytes: + _account(account_id) + if type(object_generation) is not str or _DIGEST.fullmatch(object_generation) is None: + raise InlineStorageError("INVALID_INPUT") + if type(object_ref) is not str or object_ref != _reference(account_id, object_generation): + raise InlineStorageError("NOT_FOUND") + if cancel.is_set(): + raise asyncio.CancelledError + async with self._transaction() as connection: + payload = await self._read_on(connection, account_id, object_generation) + if cancel.is_set(): + raise asyncio.CancelledError + return payload + + +class PgReportingSealStore(_PgStorage): + """First committed seal wins for each trusted account and execution key.""" + + async def _get_on(self, connection: Any, account_id: str, key: str) -> SealedSlice | None: + row = await ( + await connection.execute( + "SELECT staged_commit_ref, manifest_sha256, byte_count," + " CASE WHEN octet_length(manifest) <= %s THEN manifest END" + " FROM reporting_inline_seals WHERE account_id = %s AND source_execution_key = %s", + (SOURCE_BATCH_MANIFEST_MAX_BYTES_V1, account_id, key), + ) + ).fetchone() + if row is None: + return None + reference: SourceBatchManifestReferenceV1 | None + try: + reference = SourceBatchManifestReferenceV1( + staged_commit_ref=row[0], manifest_sha256=row[1], byte_count=row[2] + ) + except Exception: + reference = None + if reference is None: + raise InlineStorageError("INTEGRITY_FAILED") + return _seal( + account_id, + key, + SealedSlice(reference=reference, manifest_bytes=row[3]), + "INTEGRITY_FAILED", + ) + + @_redact + async def get(self, *, account_id: str, source_execution_key: str) -> SealedSlice | None: + _identity(account_id, source_execution_key) + async with self._transaction() as connection: + return await self._get_on(connection, account_id, source_execution_key) + + def _prepare_seal( + self, *, account_id: str, source_execution_key: str, sealed: SealedSlice + ) -> PreparedBackendSealV1: + _prepared_identity(account_id, source_execution_key) + candidate = _seal(account_id, source_execution_key, sealed, "INVALID_INPUT") + reference = candidate.reference + return PreparedBackendSealV1( + account_id, + source_execution_key, + reference.staged_commit_ref, + reference.manifest_sha256, + reference.byte_count, + candidate.manifest_bytes, + ) + + async def _put_on(self, connection: Any, prepared: PreparedBackendSealV1) -> SealedSlice: + """Return the verified stored winner on the caller's audited transaction. + + The neutral value proves no admission, referenced-object existence or + commit. Caller owns account locks, cancellation, settlement and any fence. + """ + if type(prepared) is not PreparedBackendSealV1: + raise InlineStorageError("INVALID_INPUT") + prepared._validate() + # Build a fresh reference, then revalidate it inside the existing closed + # candidate validator. No mutable caller model is retained or trusted. + reference = SourceBatchManifestReferenceV1.model_construct( + staged_commit_ref=prepared.staged_commit_ref, + manifest_sha256=prepared.manifest_sha256, + byte_count=prepared.byte_count, + ) + candidate = _seal( + prepared.account_id, + prepared.source_execution_key, + SealedSlice(reference=reference, manifest_bytes=prepared.manifest_bytes), + "INVALID_INPUT", + ) + await connection.execute( + "INSERT INTO reporting_inline_seals (account_id, source_execution_key," + " staged_commit_ref, manifest_sha256, byte_count, manifest)" + " VALUES (%s, %s, %s, %s, %s, %s)" + " ON CONFLICT (account_id, source_execution_key) DO NOTHING", + ( + prepared.account_id, + prepared.source_execution_key, + candidate.reference.staged_commit_ref, + candidate.reference.manifest_sha256, + candidate.reference.byte_count, + candidate.manifest_bytes, + ), + ) + winner = await self._get_on(connection, prepared.account_id, prepared.source_execution_key) + if winner is None: + raise InlineStorageError("INTEGRITY_FAILED") + return winner + + @_redact + async def put( + self, *, account_id: str, source_execution_key: str, sealed: SealedSlice + ) -> SealedSlice: + prepared = self._prepare_seal( + account_id=account_id, source_execution_key=source_execution_key, sealed=sealed + ) + async with self._transaction() as connection: + return await self._put_on(connection, prepared) diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/__init__.py new file mode 100644 index 000000000..76496e31d --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL ledger, reconciliation store, row storage and change feed.""" diff --git a/src/adcp/reporting/ledger/change_feed.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/change_feed.py similarity index 99% rename from src/adcp/reporting/ledger/change_feed.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/change_feed.py index 5dc38735a..ac1e7b6ca 100644 --- a/src/adcp/reporting/ledger/change_feed.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/change_feed.py @@ -38,7 +38,7 @@ from typing import TYPE_CHECKING, Any, Literal if TYPE_CHECKING: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore __all__ = [ "REPORTING_CHANGES_DDL_PATH", diff --git a/src/adcp/reporting/ledger/delivery_pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/delivery_pg.py similarity index 99% rename from src/adcp/reporting/ledger/delivery_pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/delivery_pg.py index 5397ac60b..3fe35ad19 100644 --- a/src/adcp/reporting/ledger/delivery_pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/delivery_pg.py @@ -20,6 +20,15 @@ unavailable, validate_transition, ) +from adcp.reporting.ledger._record_rows import ( + _ADJUSTMENT_COLUMNS, + _OBLIGATION_COLUMNS, + _REVISION_COLUMNS, + _adjustment_from_row, + _configuration_from_row, + _obligation_from_row, + _revision_from_row, +) from adcp.reporting.ledger.delivery import ( ReportingReconciliationSnapshot, _boundary_unavailable, @@ -43,18 +52,11 @@ ReportingDeliveryRecord, ReportingDestinationBinding, ) -from adcp.reporting.ledger.pg import ( - _ADJUSTMENT_COLUMNS, - _OBLIGATION_COLUMNS, - _REVISION_COLUMNS, +from adcp.reporting.ledger.store import LedgerConflictError +from adcp_reporting_storage.ledger.pg import ( PgReportingLedgerStore, - _adjustment_from_row, - _configuration_from_row, _json, - _obligation_from_row, - _revision_from_row, ) -from adcp.reporting.ledger.store import LedgerConflictError _IDENTITY_COLUMNS = ( "account_id", @@ -92,6 +94,10 @@ def _filter_params(filters: ReportingReconciliationFilter) -> tuple[Any, ...]: class PgReportingReconciliationStore(PgReportingLedgerStore, _ReconciliationOperations): + @property + def durable_reconciliation(self) -> bool: + return True + """Evidence and receipt heads commit with the feed, including autocommit pools. All state decisions run under the same per-account transaction lock as Core diff --git a/src/adcp/reporting/ledger/period_retirement.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/period_retirement.py similarity index 97% rename from src/adcp/reporting/ledger/period_retirement.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/period_retirement.py index 2ab4e0a7f..20354f3ac 100644 --- a/src/adcp/reporting/ledger/period_retirement.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/period_retirement.py @@ -44,11 +44,11 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from adcp.reporting.ledger.row_retention import NoLongerEligibleError -from adcp.reporting.ledger.row_storage import ReportingRowStoreError +from adcp_reporting_storage.ledger.row_retention import NoLongerEligibleError +from adcp_reporting_storage.ledger.row_storage import ReportingRowStoreError if TYPE_CHECKING: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore __all__ = ["ReportingPeriodRetirementV1", "retire_expired_periods"] @@ -404,19 +404,19 @@ async def _retire_period( raise NoLongerEligibleError await connection.execute(_tombstone_insert(has_rows), (obligation_id, account_id)) if has_rows: - from adcp.reporting.ledger.row_retention import mark_period_pruning + from adcp_reporting_storage.ledger.row_retention import mark_period_pruning row_set_ids = await mark_period_pruning(connection, obligation_id, account_id) # Phase 2: external bytes, at their recorded versions. if has_rows and store._row_storage is not None: - from adcp.reporting.ledger.row_retention import delete_period_objects + from adcp_reporting_storage.ledger.row_retention import delete_period_objects await delete_period_objects(store, obligation_id, row_set_ids) # Phase 3: the records, together. async with store._pool.connection() as connection, connection.transaction(): await store._lock_account(connection, account_id) if has_rows: - from adcp.reporting.ledger.row_retention import _finish_period + from adcp_reporting_storage.ledger.row_retention import _finish_period await _finish_period(connection, obligation_id, account_id, row_set_ids) try: diff --git a/src/adcp/reporting/ledger/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/pg.py similarity index 91% rename from src/adcp/reporting/ledger/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/pg.py index ef440bbed..8ab1f7ee2 100644 --- a/src/adcp/reporting/ledger/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/pg.py @@ -76,7 +76,7 @@ import base64 import hashlib import json -from collections.abc import AsyncIterator, Callable, Sequence +from collections.abc import AsyncIterator, Awaitable, Callable, Sequence from contextlib import asynccontextmanager from contextvars import ContextVar from copy import deepcopy @@ -92,7 +92,25 @@ from adcp.reporting.canonical_json import canonical_json_utf8_v1 from adcp.reporting.currency import require_frozen_currency -from adcp.reporting.evidence import ReportingCanonicalDigest, ReportingControlTotalRecord +from adcp.reporting.ledger._record_rows import ( + _ADJUSTMENT_COLUMNS, + _ISSUE_COLUMNS, + _OBLIGATION_COLUMNS, + _REVISION_COLUMNS, + _STATUS_COLUMNS, + _STATUS_COLUMNS_BARE, + _adjustment_from_row, + _configuration_from_row, + _configuration_payload, + _consumer_status_payload, + _definition_payload, + _issue_from_row, + _obligation_from_row, + _revision_from_row, + _revision_payload, + _schedule_payload, + _status_from_row, +) from adcp.reporting.ledger.health import issue_id_for_occurrence from adcp.reporting.ledger.host_reads import ReportingCurrentRevisionsPageV1 from adcp.reporting.ledger.models import ( @@ -103,12 +121,9 @@ ReportingCaller, ReportingConfiguration, ReportingConfigurationGenerationKey, - ReportingDefinitionBinding, ReportingIssueLifecycle, ReportingObligationRecord, - ReportingPeriodBoundary, ReportingRevisionRecord, - ReportingScheduleSpec, ) from adcp.reporting.ledger.notification_models import ( DirtyReason, @@ -122,12 +137,6 @@ validate_scope_refinement, ) from adcp.reporting.ledger.provisional import ProvisionalAcquisition, ProvisionalObservation -from adcp.reporting.ledger.row_storage import ( - REPORTING_ROW_STORAGE_DDL_PATH, - ReportingRowStorageOptionsV1, - ReportingRowStorageV1, - ReportingRowStoreError, -) from adcp.reporting.ledger.store import ( LeasedConfiguration, LedgerConflictError, @@ -139,23 +148,34 @@ configuration_lifecycle, encode_cursor, issue_is_retirable, - managed_revision_metadata, reject_reserved_authoritative_party, validate_adjustment_currency, validate_managed_revision_rows, validate_revision_currency, ) +from adcp_reporting_storage.ledger.row_storage import ( + REPORTING_ROW_STORAGE_DDL_PATH, + ReportingRowStorageOptionsV1, + ReportingRowStorageV1, + ReportingRowStoreError, +) if TYPE_CHECKING: from psycopg_pool import AsyncConnectionPool from adcp.reporting._source_authorization import InlineSealPublisher from adcp.reporting.inline_source import ReportingSealStore, SealedSlice - from adcp.reporting.ledger.change_feed import ReportingChangeKindV1, ReportingChangesPageV1 - from adcp.reporting.ledger.period_retirement import ReportingPeriodRetirementV1 - from adcp.reporting.ledger.row_migration import ReportingRowMigrationBatchV1 - from adcp.reporting.ledger.row_retention import ReportingRowIntentSweepV1, ReportingRowPruneV1 from adcp.reporting.ledger.status_projection import ReportingStatusSnapshot + from adcp_reporting_storage.ledger.change_feed import ( + ReportingChangeKindV1, + ReportingChangesPageV1, + ) + from adcp_reporting_storage.ledger.period_retirement import ReportingPeriodRetirementV1 + from adcp_reporting_storage.ledger.row_migration import ReportingRowMigrationBatchV1 + from adcp_reporting_storage.ledger.row_retention import ( + ReportingRowIntentSweepV1, + ReportingRowPruneV1, + ) try: from psycopg_pool import AsyncConnectionPool as _AsyncConnectionPool # noqa: F401 @@ -166,7 +186,7 @@ _INSTALL_HINT = ( "PgReportingLedgerStore requires psycopg3 and psycopg-pool. " - "Install the 'pg' extra: `pip install 'adcp[pg]'`." + "Install the 'pg' extra: `pip install 'adcp-reporting-storage[pg]'`." ) _DDL_PATH = Path(__file__).parent / "reporting_ledger.sql" @@ -282,13 +302,139 @@ async def transaction(self) -> AsyncIterator[PgReportingLedgerStore]: finally: _BOUND_CONNECTION.reset(token) + # -- what the protocol layer may ask of a durable store ------------------ + # + # ``adcp`` composes queues, projections and capability checks over this + # store without importing it; see :mod:`adcp.reporting.outbox.durable` and + # :mod:`adcp.reporting.production.store`. Exact-class checks live here so + # a subclass is refused exactly where it was refused before. + + @property + def clock(self) -> Callable[[], datetime] | None: + return self._clock + + @property + def storage_identity(self) -> object: + return self._pool + + @property + def storage_open(self) -> bool: + return self._pool.closed is False + + @property + def notifications_enabled(self) -> bool: + return self._notifications_enabled + + @property + def durable_reconciliation(self) -> bool: + return False + + def _is_core_ledger(self) -> bool: + from adcp_reporting_storage.ledger.delivery_pg import PgReportingReconciliationStore + + return type(self) in {PgReportingLedgerStore, PgReportingReconciliationStore} + + def _is_own_queue(self, outbox: object) -> bool: + from adcp_reporting_storage.outbox.pg import PgReportingOutbox + + return ( + type(outbox) is PgReportingOutbox + and self._is_core_ledger() + and outbox._pool is self._pool + and self._notifications_enabled + ) + + async def validate_notification_chain(self, outbox: object) -> None: + from adcp_reporting_storage.outbox._schema import validate_schema + + if not self._is_own_queue(outbox): + raise ReportingNotificationError("notification_chain_unready") + async with self._pool.connection() as connection, connection.transaction(): + await validate_schema(connection) + + def validate_activity_chain(self, outbox: object) -> None: + if not self._is_own_queue(outbox): + raise ReportingNotificationError("activity_chain_unready") + + def validate_composite_activity_chain( + self, *, outbox: object, status_store: object, reader: object + ) -> None: + from adcp_reporting_storage.outbox.pg import PgReportingOutbox + from adcp_reporting_storage.outbox.status_activity_pg import PgReportingActivityUnionStore + from adcp_reporting_storage.outbox.status_pg import ( + PgReportingStatusOutbox, + PgStatusNotificationStore, + ) + + if ( + type(status_store) is not PgStatusNotificationStore + or not self._is_core_ledger() + or type(outbox) is not PgReportingOutbox + or type(reader) is not PgReportingActivityUnionStore + or reader.b_outbox is not outbox + or reader.status_outbox is not status_store.outbox + or status_store.ledger is not self + or outbox._pool is not self._pool + or type(status_store.outbox) is not PgReportingStatusOutbox + or status_store.outbox._pool is not self._pool + or outbox._clock is not status_store.outbox._clock + or not self._notifications_enabled + ): + raise ReportingNotificationError("activity_chain_unready") + + def activity_schema_contract(self, *, composite: bool) -> str: + from adcp_reporting_storage.outbox import _schema + + contract = _schema._digest(_schema.REQUIRED_OBJECTS) + if composite: + from adcp_reporting_storage.outbox import status_schema + + contract += ":" + _schema._digest(status_schema.REQUIRED_STATUS_OBJECTS) + return contract + + async def validate_activity_schema(self, *, composite: bool) -> None: + from adcp_reporting_storage.outbox import _schema + + async with self._pool.connection() as connection: + try: + installed = await _schema.schema_objects(connection) + except Exception: + raise ReportingNotificationError( + "notification_schema_unready:catalog_unavailable" + ) from None + # One physical catalog capture, each required contract once. + retired = await _schema.legacy_rows_retired(connection, installed) + _schema._validate_schema_objects( + installed, activity=True, **({"legacy_rows_retired": True} if retired else {}) + ) + if composite: + from adcp_reporting_storage.outbox import status_schema + + status_schema._validate_status_objects(installed, activity=True, status=False) + + def maintenance_operations(self) -> tuple[tuple[str, Callable[[], Awaitable[Any]]], ...]: + """Retention passes for the features this store opted into, in run order.""" + operations: list[tuple[str, Callable[[], Awaitable[Any]]]] = [] + if self._row_storage is not None: + operations.append(("intent_sweep", lambda: self.sweep_row_write_intents(limit=100))) + # Frees row bytes of periods past retention even while their records stay + # (protected by append-only guards), so storage is reclaimed whether or not + # records can be. + operations.append(("row_pruning", lambda: self.prune_expired_row_sets(limit=20))) + if self._row_storage is not None or self._change_feed: + operations.append(("period_retirement", lambda: self.retire_expired_periods(limit=20))) + if self._change_feed: + operations.append(("change_pruning", lambda: self.prune_changes())) + return tuple(operations) + @asynccontextmanager - async def _source_publication( + async def source_publication( self, account_id: str, *, seals: ReportingSealStore | None = None ) -> AsyncIterator[InlineSealPublisher | None]: from psycopg import Error - from adcp.reporting.inline_storage import InlineStorageError, PgReportingSealStore + from adcp.reporting.inline_storage import InlineStorageError + from adcp_reporting_storage.inline_storage import PgReportingSealStore if isinstance(seals, PgReportingSealStore) and seals._pool is self._pool: # The backend's audited READ COMMITTED transaction owns both the @@ -363,7 +509,7 @@ async def migrate_inline_rows( ``ROWS_INTEGRITY_FAILED``. Pass the returned ``cursor`` back to resume; it is ``None`` once no candidates remain. Requires the ``row_storage`` option. """ - from adcp.reporting.ledger.row_migration import migrate_inline_rows + from adcp_reporting_storage.ledger.row_migration import migrate_inline_rows return await migrate_inline_rows( self, limit=limit, account_id=account_id, cursor=cursor, binding_id=binding_id @@ -387,7 +533,7 @@ async def sweep_row_write_intents( sets when an unswept intent is counted as ``overdue``. Schedule it alongside the producer. """ - from adcp.reporting.ledger.row_retention import sweep_row_write_intents + from adcp_reporting_storage.ledger.row_retention import sweep_row_write_intents return await sweep_row_write_intents( self, limit=limit, account_id=account_id, overdue_after_seconds=overdue_after_seconds @@ -405,9 +551,9 @@ async def prune_expired_row_sets( Rows stop being served first, then external objects are deleted at their recorded versions, then the manifests and bodies go. A failed delete leaves the period ``pruning`` with every locator intact and the next pass resumes. - See :mod:`adcp.reporting.ledger.row_retention` for what qualifies a period. + See :mod:`adcp_reporting_storage.ledger.row_retention` for what qualifies a period. """ - from adcp.reporting.ledger.row_retention import prune_expired_row_sets + from adcp_reporting_storage.ledger.row_retention import prune_expired_row_sets return await prune_expired_row_sets( self, @@ -424,7 +570,7 @@ async def changes_after( kinds: Sequence[ReportingChangeKindV1] | None = None, limit: int = 500, ) -> ReportingChangesPageV1: - """Read the shared host change feed; see :mod:`adcp.reporting.ledger.change_feed`. + """Read the shared host change feed; see :mod:`adcp_reporting_storage.ledger.change_feed`. Account feeds order by ``seq``; the deployment-wide feed orders by ``(xid, seq)`` below the reading snapshot's ``xmin``. A cursor older than the retained history @@ -432,7 +578,7 @@ async def changes_after( prune commits while the page is being read (the horizon is read again after the page). Requires ``change_feed=True``. """ - from adcp.reporting.ledger.change_feed import changes_after + from adcp_reporting_storage.ledger.change_feed import changes_after return await changes_after( self, cursor=cursor, account_id=account_id, kinds=kinds, limit=limit @@ -450,9 +596,9 @@ async def save_feed_consumer_cursor( Forward-only within a feed scope: a cursor behind the saved one is ignored and the call returns ``False``. Pass ``rewind=True`` to move the position back deliberately. - See :func:`adcp.reporting.ledger.change_feed.save_feed_consumer_cursor`. + See :func:`adcp_reporting_storage.ledger.change_feed.save_feed_consumer_cursor`. """ - from adcp.reporting.ledger.change_feed import save_feed_consumer_cursor + from adcp_reporting_storage.ledger.change_feed import save_feed_consumer_cursor return await save_feed_consumer_cursor( self, name, cursor, refresh_unchanged=refresh_unchanged, rewind=rewind @@ -460,7 +606,7 @@ async def save_feed_consumer_cursor( async def remove_feed_consumer(self, name: str) -> bool: """Unregister a consumer so it stops holding back pruning; ``True`` if it was registered.""" - from adcp.reporting.ledger.change_feed import remove_feed_consumer + from adcp_reporting_storage.ledger.change_feed import remove_feed_consumer return await remove_feed_consumer(self, name) @@ -472,7 +618,7 @@ async def prune_changes( limit: int = 10_000, ) -> int: """Delete change rows past retention that every live registered consumer has passed.""" - from adcp.reporting.ledger.change_feed import prune_changes + from adcp_reporting_storage.ledger.change_feed import prune_changes return await prune_changes( self, @@ -495,9 +641,9 @@ async def retire_expired_periods( records: ordered and resumable, so the records that locate row bytes are never deleted while the bytes remain. Periods referenced by a materialization, a receipt or an unread registered feed consumer are held. See - :mod:`adcp.reporting.ledger.period_retirement` for what Python can delete today. + :mod:`adcp_reporting_storage.ledger.period_retirement` for what Python can delete today. """ - from adcp.reporting.ledger.period_retirement import retire_expired_periods + from adcp_reporting_storage.ledger.period_retirement import retire_expired_periods return await retire_expired_periods( self, @@ -513,12 +659,12 @@ async def retire_legacy_inline_rows(self) -> bool: The table is renamed, never truncated, so binaries that predate chunked rows fail closed. Returns ``False`` when it was already retired. """ - from adcp.reporting.ledger.row_migration import retire_legacy_inline_rows + from adcp_reporting_storage.ledger.row_migration import retire_legacy_inline_rows return await retire_legacy_inline_rows(self) async def _create_schema_on(self, connection: Any) -> None: - from adcp.reporting.migration import require_owned_schema_or_empty + from adcp_reporting_storage.migration import require_owned_schema_or_empty await require_owned_schema_or_empty(connection) for path in ( @@ -541,17 +687,17 @@ async def _create_schema_on(self, connection: Any) -> None: await connection.execute(_ROW_MIGRATION_DDL_PATH.read_text()) await self._require_provisional_schema(connection) if self._notifications_enabled: - from adcp.reporting.outbox._schema import validate_schema + from adcp_reporting_storage.outbox._schema import validate_schema await validate_schema(connection) async def _require_provisional_schema(self, connection: Any) -> None: - from adcp.reporting.outbox._schema import validate_provisional_schema + from adcp_reporting_storage.outbox._schema import validate_provisional_schema await validate_provisional_schema(connection) async def _notification_now(self, connection: Any) -> datetime: - from adcp.reporting.outbox.pg import database_now + from adcp_reporting_storage.outbox.pg import database_now if self._clock is not None: # Preserve the explicit conformance clock across deferred boundary @@ -562,7 +708,7 @@ async def _notification_now(self, connection: Any) -> datetime: async def _record_notification(self, connection: Any, event: ReportingDomainEvent) -> None: if self._notifications_enabled: - from adcp.reporting.outbox.pg import enqueue_event + from adcp_reporting_storage.outbox.pg import enqueue_event await enqueue_event(connection, event) @@ -575,8 +721,8 @@ async def _dirty_status( after: ReportingStatusEvidence | None = None, ) -> None: if self._notifications_enabled: - from adcp.reporting.ledger.status_snapshot import settle_snapshot_on - from adcp.reporting.outbox.pg import mark_dirty + from adcp_reporting_storage.ledger.status_snapshot import settle_snapshot_on + from adcp_reporting_storage.outbox.pg import mark_dirty await settle_snapshot_on(self, connection, account_id=scope.account_id) await mark_dirty( @@ -738,7 +884,7 @@ async def _bound_lock_waits(connection: Any) -> None: " FROM pg_settings WHERE name='lock_timeout'" ) - def _worker_lock_errors(self) -> tuple[type[Exception], ...]: + def worker_lock_errors(self) -> tuple[type[Exception], ...]: from psycopg.errors import DeadlockDetected, LockNotAvailable bound = _BOUND_CONNECTION.get() @@ -1299,7 +1445,7 @@ async def commit_revision( if write_error is not None: raise write_error if self._row_storage is not None: - from adcp.reporting.ledger.row_write import persist_revision_row_set + from adcp_reporting_storage.ledger.row_write import persist_revision_row_set await persist_revision_row_set( self, @@ -1362,9 +1508,9 @@ async def _write_revision_row_set( """Prepare and store a revision's rows in this transaction (``postgres`` bindings only). Used by the inline-row migration, which has no upload phase. The commit path - goes through :func:`~adcp.reporting.ledger.row_write.persist_revision_row_set`. + goes through :func:`~adcp_reporting_storage.ledger.row_write.persist_revision_row_set`. """ - from adcp.reporting.ledger.row_write import prepare_revision_row_set + from adcp_reporting_storage.ledger.row_write import prepare_revision_row_set storage = self._row_storage assert storage is not None @@ -1388,7 +1534,7 @@ async def stage_revision_rows( the write intent atomically with the header. Returns whether anything was staged; replays and stores without row storage return ``False``. """ - from adcp.reporting.ledger.row_write import stage_revision_rows + from adcp_reporting_storage.ledger.row_write import stage_revision_rows if self._row_storage is None: return False @@ -2387,10 +2533,8 @@ async def record_consumer_status( if replay is not None: return replay, False - from adcp.reporting.ledger.status_snapshot import ( - read_snapshot_on, - validate_status_evidence, - ) + from adcp.reporting.ledger.status_snapshot import validate_status_evidence + from adcp_reporting_storage.ledger.status_snapshot import read_snapshot_on validate_status_evidence( status, @@ -2483,7 +2627,7 @@ async def record_consumer_status( status.reporting_status_id, consumer_id=status.consumer_id, ) - from adcp.reporting.ledger.status_snapshot import settle_snapshot_on + from adcp_reporting_storage.ledger.status_snapshot import settle_snapshot_on await settle_snapshot_on(self, connection, account_id=status.account_id) if self._notifications_enabled: @@ -2513,8 +2657,8 @@ async def record_consumer_status_with_lifecycle( async def read_status_snapshot(self, *, caller: ReportingCaller) -> ReportingStatusSnapshot: from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal - from adcp.reporting.ledger.status_snapshot import settle_snapshot_on from adcp.reporting.materializer.capture import private_snapshot + from adcp_reporting_storage.ledger.status_snapshot import settle_snapshot_on async with self._connection() as connection, connection.transaction(): await self._lock_account(connection, caller.account_id) @@ -2719,7 +2863,7 @@ async def set_issue_state( check_issue_state_transition(live.issue_state, state) if state == "waived" and live.issue_state != "waived": from adcp.reporting.ledger.status_projection import bind_mismatch_waiver - from adcp.reporting.ledger.status_snapshot import read_snapshot_on + from adcp_reporting_storage.ledger.status_snapshot import read_snapshot_on snapshot = await read_snapshot_on( connection, @@ -3290,22 +3434,6 @@ def _translate_integrity_error(error: Exception) -> LedgerConflictError: return LedgerConflictError("LEDGER_WRITE_FAILED", "the ledger write violated an integrity rule") -_OBLIGATION_COLUMNS = ( - "reporting_obligation_id, account_id, delivery_config_id, delivery_config_version," - " report_definition_id, reporting_profile, feed_purpose, period_key, period_start," - " period_end, source_timezone, expected_at, scope_resolved_at," - " automated_recovery_deadline_at, required_finality, coverage_status, media_buy_ids," - " package_ids, schedule, definition, created_at, currency, consumer_id" -) - -_REVISION_COLUMNS = ( - "reporting_revision_id, account_id, reporting_obligation_id, finality," - " revision_content_sha256, row_count, control_totals, observed_at, data_through," - " created_at, supersedes_reporting_revision_id, finality_basis, finality_policy_id," - " finalized_at, readable, readable_at_commit, source_publication_id, source_manifest_sha256," - " canonical_content_digest, managed_control_totals" -) - _OUTER_REVISION_COLUMNS = ", ".join(f"r.{name.strip()}" for name in _REVISION_COLUMNS.split(",")) # The current revision of an obligation is the official one if it exists, otherwise the # snapshot nothing supersedes yet. An official revision is terminal, so it is never superseded. @@ -3317,44 +3445,6 @@ def _translate_integrity_error(error: Exception) -> LedgerConflictError: " ORDER BY (r.finality = 'official') DESC, r.created_at DESC, r.reporting_revision_id DESC" ) -_ADJUSTMENT_COLUMNS = ( - "reporting_adjustment_id, account_id, adjusts_reporting_revision_id, reason_code," - " reason_detail, accounting_period_start, accounting_period_end, control_total_deltas," - " correction_observed_at, created_at, managed_control_total_deltas" -) - -_STATUS_COLUMNS = ( - "s.reporting_status_id, s.account_id, s.consumer_id, s.delivery_config_id," - " s.delivery_config_version, s.report_definition_id, s.period_start, s.period_end," - " s.period_source_timezone, s.consumer_status, s.status_as_of, s.recorded_at," - " s.supersedes_reporting_status_id, s.reporting_obligation_id, s.reporting_revision_id," - " s.observed_revision_content_sha256, s.failure_code, s.mismatch_code," - " s.consumer_commit_ref, s.seller_ledger_snapshot_id, s.seller_ledger_as_of, s.superseded" -) - -_STATUS_COLUMNS_BARE = _STATUS_COLUMNS.replace("s.", "") - -_ISSUE_COLUMNS = ( - "issue_key, account_id, generation, issue_id, consumer_id, opened_at, issue_state," - " external_ref, retired_at" -) - - -def _issue_from_row(row: Sequence[Any]) -> ReportingIssueLifecycle: - return ReportingIssueLifecycle( - issue_key=row[0], - account_id=row[1], - generation=row[2], - issue_id=row[3], - consumer_id=row[4], - opened_at=_utc(row[5]), - issue_state=row[6], - external_ref=row[7], - retired_at=_utc(row[8]) if row[8] else None, - waived_reporting_status_id=row[9] if len(row) > 9 else None, - waived_conflict_sha256=row[10] if len(row) > 10 else None, - ) - async def _waiver_storage_on(connection: Any) -> bool: row = await ( @@ -3410,230 +3500,6 @@ async def _save_waiver_binding(connection: Any, issue: ReportingIssueLifecycle) raise LedgerConflictError("WAIVER_BINDING_IMMUTABLE", "a recorded waiver cannot be rebound") -def _schedule_payload(schedule: ReportingScheduleSpec) -> dict[str, Any]: - return { - "period_duration": schedule.period_duration, - "delivery_sla": schedule.delivery_sla, - "alignment": schedule.alignment, - "period_timezone": schedule.period_timezone, - "period_anchor": ( - _utc(schedule.period_anchor).isoformat() if schedule.period_anchor else None - ), - } - - -def _definition_payload( - definition: ReportingDefinitionBinding | None, -) -> dict[str, Any] | None: - return definition.to_storage() if definition is not None else None - - -def _definition_from_payload(payload: dict[str, Any] | None) -> ReportingDefinitionBinding | None: - return ReportingDefinitionBinding(**payload) if payload else None - - -def _schedule_from_payload(payload: dict[str, Any]) -> ReportingScheduleSpec: - anchor = payload.get("period_anchor") - return ReportingScheduleSpec( - period_duration=payload["period_duration"], - delivery_sla=payload["delivery_sla"], - alignment=payload.get("alignment", "utc"), - period_timezone=payload.get("period_timezone"), - period_anchor=datetime.fromisoformat(anchor) if anchor else None, - ) - - -def _configuration_payload(configuration: ReportingConfiguration) -> dict[str, Any]: - return { - "account_id": configuration.account_id, - "report_definition_id": configuration.report_definition_id, - "reporting_profile": configuration.reporting_profile, - "feed_purpose": configuration.feed_purpose, - "required_finality": configuration.required_finality, - "account_timezone": configuration.account_timezone, - "media_buy_ids": sorted(configuration.media_buy_ids), - "schedule": _schedule_payload(configuration.schedule), - "definition": _definition_payload(configuration.definition), - } - - -def _configuration_from_row(row: Sequence[Any]) -> ReportingConfiguration: - return ReportingConfiguration( - delivery_config_id=row[0], - delivery_config_version=row[1], - account_id=row[2], - report_definition_id=row[3], - reporting_profile=row[4], - feed_purpose=row[5], - required_finality=row[6], - account_timezone=row[7], - schedule=_schedule_from_payload(row[8]), - media_buy_ids=tuple(row[9] or ()), - activated_at=_utc(row[10]) if row[10] else None, - deactivated_at=_utc(row[11]) if row[11] else None, - automated_recovery_window=timedelta(seconds=float(row[12])), - status_retention_days=row[13], - definition=_definition_from_payload(row[14]), - authoritative_party=row[15], - consumer_id=row[16], - quarantined=row[17], - ) - - -def _obligation_from_row(row: Sequence[Any]) -> ReportingObligationRecord: - return ReportingObligationRecord( - reporting_obligation_id=row[0], - account_id=row[1], - delivery_config_id=row[2], - delivery_config_version=row[3], - report_definition_id=row[4], - reporting_profile=row[5], - feed_purpose=row[6], - period=ReportingPeriodBoundary( - period_key=row[7], - start=_utc(row[8]), - end=_utc(row[9]), - source_timezone=row[10], - expected_at=_utc(row[11]), - ), - scope_resolved_at=_utc(row[12]), - automated_recovery_deadline_at=_utc(row[13]), - required_finality=row[14], - coverage_status=row[15], - media_buy_ids=tuple(row[16] or ()), - package_ids=tuple(row[17] or ()), - schedule=_schedule_from_payload(row[18]), - definition=_definition_from_payload(row[19]), - created_at=_utc(row[20]), - currency=row[21], - consumer_id=row[22], - ) - - -def _revision_from_row(row: Sequence[Any]) -> ReportingRevisionRecord: - return ReportingRevisionRecord( - reporting_revision_id=row[0], - account_id=row[1], - reporting_obligation_id=row[2], - finality=row[3], - revision_content_sha256=row[4], - row_count=int(row[5]), - control_totals=tuple((name, value) for name, value in (row[6] or [])), - observed_at=_utc(row[7]), - data_through=_utc(row[8]) if row[8] else None, - created_at=_utc(row[9]), - supersedes_reporting_revision_id=row[10], - finality_basis=row[11], - finality_policy_id=row[12], - finalized_at=_utc(row[13]) if row[13] else None, - readable=row[14], - readable_at_commit=row[15], - source_publication_id=row[16], - source_manifest_sha256=row[17], - canonical_content_digest=( - ReportingCanonicalDigest.from_wire(row[18]) if row[18] is not None else None - ), - managed_control_totals=( - tuple(ReportingControlTotalRecord.from_wire(item) for item in row[19]) - if row[19] is not None - else None - ), - ) - - -def _adjustment_from_row(row: Sequence[Any]) -> ReportingAdjustmentRecord: - return ReportingAdjustmentRecord( - reporting_adjustment_id=row[0], - account_id=row[1], - adjusts_reporting_revision_id=row[2], - reason_code=row[3], - reason_detail=row[4], - accounting_period_start=_utc(row[5]), - accounting_period_end=_utc(row[6]), - control_total_deltas=tuple((name, value) for name, value in (row[7] or [])), - correction_observed_at=_utc(row[8]), - created_at=_utc(row[9]), - managed_control_total_deltas=( - tuple(ReportingControlTotalRecord.from_wire(item) for item in row[10]) - if row[10] is not None - else None - ), - ) - - -def _status_from_row(row: Sequence[Any]) -> ConsumerStatusRecord: - return ConsumerStatusRecord( - reporting_status_id=row[0], - account_id=row[1], - consumer_id=row[2], - delivery_config_id=row[3], - delivery_config_version=row[4], - report_definition_id=row[5], - period_start=_utc(row[6]), - period_end=_utc(row[7]), - period_source_timezone=row[8], - consumer_status=row[9], - status_as_of=_utc(row[10]), - recorded_at=_utc(row[11]), - supersedes_reporting_status_id=row[12], - reporting_obligation_id=row[13], - reporting_revision_id=row[14], - observed_revision_content_sha256=row[15], - failure_code=row[16], - mismatch_code=row[17], - consumer_commit_ref=row[18], - seller_ledger_snapshot_id=row[19], - seller_ledger_as_of=_utc(row[20]) if row[20] else None, - superseded=row[21], - ) - - -def _consumer_status_payload(status: ConsumerStatusRecord) -> dict[str, Any]: - payload: dict[str, Any] = { - "chain": list(status.chain_key), - "consumer_status": status.consumer_status, - "status_as_of": _utc(status.status_as_of).isoformat(), - "supersedes": status.supersedes_reporting_status_id, - "obligation": status.reporting_obligation_id, - "revision": status.reporting_revision_id, - "digest": status.observed_revision_content_sha256, - "failure_code": status.failure_code, - } - # Conditional on purpose. ``canonical_json_utf8_v1`` encodes ``None`` as - # ``null``, so including the key unconditionally would change the digest of - # every statement that has no mismatch_code -- i.e. every row an rc.2 SDK - # wrote. After an in-place upgrade a buyer's exact retry would then fail - # with STATUS_IDENTITY_CONFLICT instead of replaying as ``unchanged``, - # which is the one thing an idempotent append-only surface must never do. - # Omitting the key when absent keeps rc.2 digests byte-identical while a - # code-only change is still a conflict: "the metric is missing" and "the - # currency is wrong" are different claims and must not share one immutable - # identity. - if status.mismatch_code is not None: - payload["mismatch_code"] = status.mismatch_code - return payload - - -def _revision_payload(revision: ReportingRevisionRecord) -> dict[str, Any]: - payload = { - "finality": revision.finality, - "revision_content_sha256": revision.revision_content_sha256, - "row_count": revision.row_count, - "control_totals": [list(item) for item in revision.control_totals], - "obligation": revision.reporting_obligation_id, - "supersedes": revision.supersedes_reporting_revision_id, - } - if revision.canonical_content_digest is not None: - payload["canonical_content_digest"] = revision.canonical_content_digest.to_wire() - if revision.managed_control_totals is not None: - payload["managed_control_totals"] = [ - item.to_wire() for item in revision.managed_control_totals - ] - if revision.canonical_content_digest is not None or revision.managed_control_totals is not None: - payload["managed_metadata"] = managed_revision_metadata(revision) - return payload - - _RESOLVERS: dict[str, tuple[str, str, str, Any]] = { "obligation": ( "reporting_obligations", diff --git a/src/adcp/reporting/ledger/reporting_buyer_submissions.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_buyer_submissions.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_buyer_submissions.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_buyer_submissions.sql diff --git a/src/adcp/reporting/ledger/reporting_caller_ownership.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_caller_ownership.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_caller_ownership.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_caller_ownership.sql diff --git a/src/adcp/reporting/ledger/reporting_changes.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_changes.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_changes.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_changes.sql diff --git a/src/adcp/reporting/ledger/reporting_feed.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_feed.sql similarity index 96% rename from src/adcp/reporting/ledger/reporting_feed.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_feed.sql index fd3dc69c5..ee8d79771 100644 --- a/src/adcp/reporting/ledger/reporting_feed.sql +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_feed.sql @@ -1,7 +1,7 @@ -- B2.3 isolated immutable snapshots. No old catalog object is changed. DO $migration$ BEGIN - PERFORM pg_advisory_xact_lock(hashtext('adcp.reporting.feed.schema')); + PERFORM pg_advisory_xact_lock(hashtext('adcp_reporting_storage.feed.schema')); CREATE TABLE IF NOT EXISTS reporting_feed_snapshots ( account_id TEXT COLLATE "C" NOT NULL, consumer_id TEXT COLLATE "C" NOT NULL, diff --git a/src/adcp/reporting/ledger/reporting_inline_storage.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_inline_storage.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_inline_storage.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_inline_storage.sql diff --git a/src/adcp/reporting/ledger/reporting_ledger.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_ledger.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger.sql diff --git a/src/adcp/reporting/ledger/reporting_ledger_account_generations.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_account_generations.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_ledger_account_generations.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_account_generations.sql diff --git a/src/adcp/reporting/ledger/reporting_ledger_obligation_currency.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_obligation_currency.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_ledger_obligation_currency.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_obligation_currency.sql diff --git a/src/adcp/reporting/ledger/reporting_ledger_reconciliation.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_reconciliation.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_ledger_reconciliation.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_ledger_reconciliation.sql diff --git a/src/adcp/reporting/ledger/reporting_materializer.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_materializer.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_materializer.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_materializer.sql diff --git a/src/adcp/reporting/ledger/reporting_notification_outbox.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_notification_outbox.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_notification_outbox.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_notification_outbox.sql diff --git a/src/adcp/reporting/ledger/reporting_period_retirement.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_period_retirement.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_period_retirement.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_period_retirement.sql diff --git a/src/adcp/reporting/ledger/reporting_production.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_production.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_production.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_production.sql diff --git a/src/adcp/reporting/ledger/reporting_projection.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_projection.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection.sql diff --git a/src/adcp/reporting/ledger/reporting_projection_feed.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection_feed.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_projection_feed.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection_feed.sql diff --git a/src/adcp/reporting/ledger/reporting_projection_notifications.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection_notifications.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_projection_notifications.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_projection_notifications.sql diff --git a/src/adcp/reporting/ledger/reporting_provisional_observations.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_provisional_observations.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_provisional_observations.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_provisional_observations.sql diff --git a/src/adcp/reporting/ledger/reporting_receipt_ingestion.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_receipt_ingestion.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_receipt_ingestion.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_receipt_ingestion.sql diff --git a/src/adcp/reporting/ledger/reporting_row_migration.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_row_migration.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_row_migration.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_row_migration.sql diff --git a/src/adcp/reporting/ledger/reporting_row_storage.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_row_storage.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_row_storage.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_row_storage.sql diff --git a/src/adcp/reporting/ledger/reporting_status_notifications.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_status_notifications.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_status_notifications.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_status_notifications.sql diff --git a/src/adcp/reporting/ledger/reporting_status_selector_version.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_status_selector_version.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_status_selector_version.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_status_selector_version.sql diff --git a/src/adcp/reporting/ledger/reporting_webhook_activity.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_webhook_activity.sql similarity index 100% rename from src/adcp/reporting/ledger/reporting_webhook_activity.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/reporting_webhook_activity.sql diff --git a/src/adcp/reporting/ledger/required_provisional_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/required_provisional_schema.json similarity index 100% rename from src/adcp/reporting/ledger/required_provisional_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/required_provisional_schema.json diff --git a/src/adcp/reporting/ledger/required_row_storage_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/required_row_storage_schema.json similarity index 100% rename from src/adcp/reporting/ledger/required_row_storage_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/required_row_storage_schema.json diff --git a/src/adcp/reporting/ledger/row_migration.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_migration.py similarity index 97% rename from src/adcp/reporting/ledger/row_migration.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_migration.py index be65d3f99..a0f0f971f 100644 --- a/src/adcp/reporting/ledger/row_migration.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_migration.py @@ -25,11 +25,14 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Literal -from adcp.reporting.ledger.row_storage import REPORTING_ROW_STORAGE_TABLES, ReportingRowStoreError from adcp.reporting.ledger.store import LedgerConflictError +from adcp_reporting_storage.ledger.row_storage import ( + REPORTING_ROW_STORAGE_TABLES, + ReportingRowStoreError, +) if TYPE_CHECKING: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore __all__ = [ "LEGACY_ROW_ARCHIVE_TABLE", @@ -127,7 +130,7 @@ async def migrate_inline_rows( async def _migrate_one( store: PgReportingLedgerStore, revision_id: str, account_id: str, binding_id: str | None ) -> Literal["migrated", "skipped"] | ReportingRowQuarantineReason: - from adcp.reporting.ledger import pg as pg_module + from adcp.reporting.ledger import _record_rows as pg_module async with store._pool.connection() as connection, connection.transaction(): await store._lock_account(connection, account_id) diff --git a/src/adcp/reporting/ledger/row_retention.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_retention.py similarity index 99% rename from src/adcp/reporting/ledger/row_retention.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_retention.py index 75cb14c2d..1834aa4b2 100644 --- a/src/adcp/reporting/ledger/row_retention.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_retention.py @@ -32,10 +32,10 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from adcp.reporting.ledger.row_storage import ReportingRowStoreError +from adcp_reporting_storage.ledger.row_storage import ReportingRowStoreError if TYPE_CHECKING: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore __all__ = [ "ReportingRowIntentSweepV1", diff --git a/src/adcp/reporting/ledger/row_storage.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage.py similarity index 99% rename from src/adcp/reporting/ledger/row_storage.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage.py index f8b23b18e..39d7ef542 100644 --- a/src/adcp/reporting/ledger/row_storage.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage.py @@ -54,7 +54,7 @@ ReportingRowStoreError, ReportingRowStoreErrorCode, ) -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( REPORTING_ROW_DEFAULT_KEY_TEMPLATE, ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, @@ -553,10 +553,12 @@ async def _verify_catalog(connection: Any) -> None: """ from importlib.resources import files - from adcp.reporting.outbox._schema import _catalog_objects + from adcp_reporting_storage.outbox._schema import _catalog_objects required: dict[str, dict[str, Any]] = json.loads( - files("adcp.reporting.ledger").joinpath("required_row_storage_schema.json").read_text() + files("adcp_reporting_storage.ledger") + .joinpath("required_row_storage_schema.json") + .read_text() ) installed = await _catalog_objects( connection, diff --git a/src/adcp/reporting/ledger/row_storage_azure.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_azure.py similarity index 98% rename from src/adcp/reporting/ledger/row_storage_azure.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_azure.py index 439a699fa..a2d12e811 100644 --- a/src/adcp/reporting/ledger/row_storage_azure.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_azure.py @@ -1,4 +1,4 @@ -"""Azure Blob Storage row-object provider (``adcp[reporting-azure]``). +"""Azure Blob Storage row-object provider (``adcp-reporting-storage[azure]``). Inject official ``azure.storage.blob.BlobServiceClient`` instances created by the host with managed or workload identity. Bindings use ``location={"account": ..., "container": @@ -27,7 +27,7 @@ from typing import Any, Literal from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, @@ -37,7 +37,7 @@ ReportingRowObjectStatInputV1, ReportingRowObjectStatResultV1, ) -from adcp.reporting.ledger.row_storage_provider_io import ( +from adcp_reporting_storage.ledger.row_storage_provider_io import ( assert_location_keys, assert_read_bounds, client_map, diff --git a/src/adcp/reporting/ledger/row_storage_gcs.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_gcs.py similarity index 98% rename from src/adcp/reporting/ledger/row_storage_gcs.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_gcs.py index e917384e9..782465687 100644 --- a/src/adcp/reporting/ledger/row_storage_gcs.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_gcs.py @@ -1,4 +1,4 @@ -"""Google Cloud Storage row-object provider (``adcp[reporting-gcs]``). +"""Google Cloud Storage row-object provider (``adcp-reporting-storage[gcs]``). Inject official ``google.cloud.storage.Client`` instances configured by the host with keyless credentials (workload identity, ADC). Bindings use ``location={"bucket": ...}``; @@ -20,7 +20,7 @@ from typing import Any, Literal from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, @@ -30,7 +30,7 @@ ReportingRowObjectStatInputV1, ReportingRowObjectStatResultV1, ) -from adcp.reporting.ledger.row_storage_provider_io import ( +from adcp_reporting_storage.ledger.row_storage_provider_io import ( assert_key, assert_location_keys, assert_read_bounds, diff --git a/src/adcp/reporting/ledger/row_storage_object.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_object.py similarity index 100% rename from src/adcp/reporting/ledger/row_storage_object.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_object.py diff --git a/src/adcp/reporting/ledger/row_storage_provider_io.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_provider_io.py similarity index 97% rename from src/adcp/reporting/ledger/row_storage_provider_io.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_provider_io.py index ab460bfb6..d59b6cc46 100644 --- a/src/adcp/reporting/ledger/row_storage_provider_io.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_provider_io.py @@ -10,7 +10,7 @@ from typing import Any, TypeVar from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ReportingRowObjectGetInputV1 +from adcp_reporting_storage.ledger.row_storage_object import ReportingRowObjectGetInputV1 __all__ = [ "assert_key", diff --git a/src/adcp/reporting/ledger/row_storage_s3.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_s3.py similarity index 98% rename from src/adcp/reporting/ledger/row_storage_s3.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_s3.py index 7b3b53b94..d3d96e423 100644 --- a/src/adcp/reporting/ledger/row_storage_s3.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_storage_s3.py @@ -1,4 +1,4 @@ -"""Amazon S3 and S3-compatible row-object provider (``adcp[reporting-s3]``). +"""Amazon S3 and S3-compatible row-object provider (``adcp-reporting-storage[s3]``). Inject official ``boto3`` S3 clients configured by the host (region, endpoint, credentials, path style). Bindings use ``location={"bucket": ..., "region": ...}``; @@ -26,7 +26,7 @@ from typing import Any, Literal from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, @@ -36,7 +36,7 @@ ReportingRowObjectStatInputV1, ReportingRowObjectStatResultV1, ) -from adcp.reporting.ledger.row_storage_provider_io import ( +from adcp_reporting_storage.ledger.row_storage_provider_io import ( assert_key, assert_location_keys, assert_read_bounds, diff --git a/src/adcp/reporting/ledger/row_write.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_write.py similarity index 98% rename from src/adcp/reporting/ledger/row_write.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_write.py index c26531cb4..7f3624993 100644 --- a/src/adcp/reporting/ledger/row_write.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/row_write.py @@ -37,18 +37,18 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any -from adcp.reporting.ledger.row_storage import ( +from adcp.reporting.ledger.row_storage_errors import ReportingRowBindingMismatchError +from adcp.reporting.ledger.store import LedgerConflictError +from adcp_reporting_storage.ledger.row_storage import ( ReportingObjectRowBindingV1, ReportingPreparedRowSetV1, ReportingRowChunkLocatorV1, ReportingRowStoreError, ) -from adcp.reporting.ledger.row_storage_errors import ReportingRowBindingMismatchError -from adcp.reporting.ledger.store import LedgerConflictError if TYPE_CHECKING: from adcp.reporting.ledger.models import ReportingRevisionRecord - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore __all__ = [ "StagedRevisionRows", @@ -148,7 +148,7 @@ async def stage_revision_rows( upload. Never call this inside a store transaction: it takes the account lock briefly on its own connection and uploads without holding it. """ - from adcp.reporting.ledger.pg import _BOUND_CONNECTION + from adcp_reporting_storage.ledger.pg import _BOUND_CONNECTION storage = store._row_storage if storage is None: diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/status_snapshot.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/status_snapshot.py new file mode 100644 index 000000000..b0c9ccd36 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/ledger/status_snapshot.py @@ -0,0 +1,206 @@ +"""Connection-bound status snapshot reads and lifecycle persistence over PostgreSQL. + +The pure projection helpers (memory snapshots, evidence validation, decoding a +stored snapshot) are :mod:`adcp.reporting.ledger.status_snapshot`; these +functions run on a caller-owned connection inside the account transaction. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +from typing import TYPE_CHECKING, Any + +from adcp.reporting.ledger.notification_models import ReportingNotificationError +from adcp.reporting.ledger.status_projection import ( + ReportingStatusSnapshot, + StatusLifecycleIntent, + apply_intents_to_snapshot, + lifecycle_intents, +) +from adcp.reporting.ledger.status_snapshot import snapshot_from_storage + +if TYPE_CHECKING: + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore + +__all__ = [ + "apply_intents_on", + "persist_replay_lifecycles_on", + "read_snapshot_on", + "settle_snapshot_on", +] + + +_TABLES = { + "configurations": "reporting_configurations", + "obligations": "reporting_obligations", + "revisions": "reporting_revisions", + "statuses": "reporting_consumer_statuses", + "lifecycles": "reporting_issue_lifecycle", + "waivers": "reporting_issue_waiver_bindings", + "issue_scopes": "reporting_issue_status_scopes", + "adjustments": "reporting_adjustments", + "changes": "reporting_ledger_changes", + "consumers": "reporting_reconciliation_heads", +} + + +async def read_snapshot_on( + connection: Any, + *, + account_id: str, + clock: Callable[[], datetime] | None = None, + as_of: datetime | None = None, + include_issue_scopes: bool = True, +) -> ReportingStatusSnapshot: + from adcp_reporting_storage.ledger.pg import _waiver_storage_on + from adcp_reporting_storage.outbox.pg import database_now + + at = as_of or await database_now(connection, clock) + raw: dict[str, Any] = {"account_id": account_id, "as_of": at.isoformat()} + for key, table in _TABLES.items(): + if key == "issue_scopes" and not include_issue_scopes: + continue + if key == "waivers" and not await _waiver_storage_on(connection): + continue + # Table names are the closed SDK constants above. Parameters are bound. + rows = await ( + await connection.execute( + f"SELECT to_jsonb(r) FROM {table} r WHERE account_id = %s", # nosec B608 + (account_id,), + ) + ).fetchall() + raw[key] = [row[0] for row in rows] + return snapshot_from_storage(raw) + + +async def apply_intents_on( + store: PgReportingLedgerStore, connection: Any, intents: tuple[StatusLifecycleIntent, ...] +) -> None: + """No pooled calls. The source/projector owns account and checkpoint locks.""" + for intent in intents: + issue = intent.lifecycle + if intent.action == "ensure_mismatch": + await store._ensure_issue_opened_on( + connection, + issue_key=issue.issue_key, + account_id=issue.account_id, + consumer_id=issue.consumer_id, + observed_at=issue.opened_at, + status_scope=intent.scope, + enqueue=False, + ) + elif intent.action == "retire_mismatch": + assert issue.retired_at is not None + await store._retire_issue_on( + connection, + issue_key=issue.issue_key, + account_id=issue.account_id, + at=issue.retired_at, + status_scope=intent.scope, + enqueue=False, + ) + else: + await store._dirty_issue(connection, issue, intent.scope, enqueue=False) + + +async def persist_replay_lifecycles_on( + store: PgReportingLedgerStore, connection: Any, snapshot: ReportingStatusSnapshot +) -> None: + """Persist derived historical occurrences without regressing later evidence. + + Captured source rows remain immutable. These are the same public lifecycle + rows used by polling, including repairs for an old writer's interrupted + status/issue pair. The enclosing account lock fences concurrent ingestion. + """ + from adcp_reporting_storage.ledger.pg import _save_waiver_binding + + scopes = dict(snapshot.issue_scopes) + for issue in sorted(snapshot.lifecycles, key=lambda i: (i.issue_key, i.generation)): + await connection.execute( + "INSERT INTO reporting_issue_lifecycle" + " (account_id, issue_key, generation, issue_id, consumer_id, opened_at," + " issue_state, external_ref, retired_at) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)" + " ON CONFLICT (account_id, issue_key, generation) DO NOTHING", + ( + issue.account_id, + issue.issue_key, + issue.generation, + issue.issue_id, + issue.consumer_id, + issue.opened_at, + issue.issue_state, + issue.external_ref, + issue.retired_at, + ), + ) + await _save_waiver_binding(connection, issue) + if issue.issue_state != "open": + await connection.execute( + "UPDATE reporting_issue_lifecycle SET issue_state=%s, external_ref=%s," + " retired_at=%s" + " WHERE account_id=%s AND issue_key=%s AND generation=%s" + " AND issue_state IN ('open','acknowledged')", + ( + issue.issue_state, + issue.external_ref, + issue.retired_at, + issue.account_id, + issue.issue_key, + issue.generation, + ), + ) + scope = scopes.get(issue.issue_id) + if scope is not None: + # Existing scope may already be more precise because a later source + # transaction attached this occurrence. Historical replay retains it. + row = await ( + await connection.execute( + "SELECT scope FROM reporting_issue_status_scopes" + " WHERE account_id=%s AND issue_id=%s", + (issue.account_id, issue.issue_id), + ) + ).fetchone() + if row is not None: + from adcp.reporting.ledger.notification_models import ( + decode_status_scope, + validate_scope_refinement, + ) + + old = decode_status_scope(row[0]) + try: + validate_scope_refinement(old, scope) + except ReportingNotificationError: + validate_scope_refinement(scope, old) + scope = old + await store._dirty_issue(connection, issue, scope, enqueue=False) + + +async def settle_snapshot_on( + store: PgReportingLedgerStore, + connection: Any, + *, + account_id: str, + as_of: datetime | None = None, +) -> ReportingStatusSnapshot: + from adcp_reporting_storage.outbox.pg import database_now + + at = as_of or await database_now(connection, store._clock) + has_scope_storage = await store._issue_scope_storage_on(connection) + for _ in range(3): + snapshot = await read_snapshot_on( + connection, account_id=account_id, as_of=at, include_issue_scopes=has_scope_storage + ) + if not has_scope_storage: + # Pre-outbox Core schemas retain lifecycle but have no scope table. + # Derive scope from authenticated status evidence for this read; + # notification-enabled participants require durable scope storage. + snapshot = apply_intents_to_snapshot( + snapshot, + tuple(i for i in lifecycle_intents(snapshot) if i.action == "refine_scope"), + ) + intents = lifecycle_intents(snapshot) + if not intents: + return snapshot + await apply_intents_on(store, connection, intents) + raise ReportingNotificationError("status_lifecycle_did_not_converge") diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/__init__.py new file mode 100644 index 000000000..de17b58d9 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL materializer store.""" diff --git a/src/adcp/reporting/materializer/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/pg.py similarity index 96% rename from src/adcp/reporting/materializer/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/pg.py index 50509342a..c983b5d9d 100644 --- a/src/adcp/reporting/materializer/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/pg.py @@ -9,6 +9,13 @@ from uuid import uuid4 from adcp.reporting.ledger._delivery_state import RecordT +from adcp.reporting.ledger._record_rows import ( + _OBLIGATION_COLUMNS, + _REVISION_COLUMNS, + _configuration_from_row, + _obligation_from_row, + _revision_from_row, +) from adcp.reporting.ledger.delivery_models import ( ReportingDeliveryPrincipal, ReportingDeliveryScope, @@ -17,15 +24,11 @@ ReportingMaterializationRecord, ReportingObligationDeliveryRecord, ) -from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore from adcp.reporting.ledger.models import ReportingConfigurationGenerationKey from adcp.reporting.ledger.notification_events import materialization_event -from adcp.reporting.ledger.pg import ( - _OBLIGATION_COLUMNS, - _REVISION_COLUMNS, - _configuration_from_row, - _obligation_from_row, - _revision_from_row, +from adcp.reporting.ledger.notification_models import ( + ReportingDomainEvent, + ReportingNotificationError, ) from adcp.reporting.ledger.store import LedgerConflictError from adcp.reporting.materializer._errors import ( @@ -35,7 +38,6 @@ from adcp.reporting.materializer.capture import ( ReportingMaterializerBoundary, decode_materializer_boundary, - enqueue_materializer_event_on, private_snapshot, ) from adcp.reporting.materializer.contracts import ( @@ -47,7 +49,6 @@ binding_fingerprint, failure, ) -from adcp.reporting.materializer.schema import validate_materializer_schema from adcp.reporting.materializer.verification import ( ReportingVerifiedDestination, validate_materialization_target, @@ -63,6 +64,28 @@ validate_lease_seconds, verification_key_id, ) +from adcp_reporting_storage.ledger.delivery_pg import PgReportingReconciliationStore +from adcp_reporting_storage.materializer.schema import validate_materializer_schema +from adcp_reporting_storage.outbox.pg import enqueue_event + + +class _MaterializerQueueConnection: + """Closed table substitution, preserving the finish transaction's connection.""" + + def __init__(self, connection: Any) -> None: + self.connection = connection + + async def execute(self, query: str, params: Any = None) -> Any: + return await self.connection.execute( + query.replace("reporting_notification_", "reporting_materializer_notification_"), params + ) + + +async def enqueue_materializer_event_on(connection: Any, event: ReportingDomainEvent) -> None: + if event.notification_type != "reporting.delivery_ready": + raise ReportingNotificationError("materializer_event_invalid") + await enqueue_event(_MaterializerQueueConnection(connection), event) + _SCOPE_WHERE = ( "account_id=%s AND consumer_id=%s AND delivery_config_id=%s" @@ -133,7 +156,9 @@ async def create_schema(self) -> None: async with self._connection() as connection, connection.transaction(): await self._create_schema_on(connection) await connection.execute( - files("adcp.reporting.ledger").joinpath("reporting_materializer.sql").read_text() + files("adcp_reporting_storage.ledger") + .joinpath("reporting_materializer.sql") + .read_text() ) @materializer_errors @@ -831,8 +856,8 @@ async def _materializer_dirty_on( await self._dirty_status(connection, scope, reason, after=evidence) from adcp.reporting.canonical_json import canonical_json_sha256_v1 - from adcp.reporting.ledger.pg import _json - from adcp.reporting.ledger.status_snapshot import settle_snapshot_on + from adcp_reporting_storage.ledger.pg import _json + from adcp_reporting_storage.ledger.status_snapshot import settle_snapshot_on core = await settle_snapshot_on( self, connection, account_id=scope.account_id, as_of=outcome.completed_at diff --git a/src/adcp/reporting/materializer/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/required_schema.json similarity index 100% rename from src/adcp/reporting/materializer/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/required_schema.json diff --git a/src/adcp/reporting/materializer/schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/schema.py similarity index 93% rename from src/adcp/reporting/materializer/schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/schema.py index dfea0b742..bfc4fcb49 100644 --- a/src/adcp/reporting/materializer/schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/materializer/schema.py @@ -7,7 +7,7 @@ from typing import Any from adcp.reporting.ledger.store import LedgerConflictError -from adcp.reporting.outbox._schema import ( +from adcp_reporting_storage.outbox._schema import ( REQUIRED_OBJECTS, is_legacy_row_object, legacy_rows_retired, @@ -18,7 +18,7 @@ async def validate_materializer_schema(connection: Any, *, notifications: bool = False) -> None: required = json.loads( - files("adcp.reporting.materializer").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.materializer").joinpath("required_schema.json").read_text() ) if not required: raise LedgerConflictError("MATERIALIZER_SCHEMA_UNREADY", "install the materializer schema") diff --git a/src/adcp/reporting/migration.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/migration.py similarity index 99% rename from src/adcp/reporting/migration.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/migration.py index aeebc5d9c..226a29af2 100644 --- a/src/adcp/reporting/migration.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/migration.py @@ -554,7 +554,7 @@ async def migrate_legacy_reporting( sql.SQL(kind), sql.Identifier(archive_schema) ) ) - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") for name in ( "reporting_ledger.sql", "reporting_ledger_account_generations.sql", @@ -576,7 +576,7 @@ async def migrate_legacy_reporting( ): await connection.execute(root.joinpath(name).read_text()) await connection.execute( - files("adcp.reporting.production").joinpath("service_context.sql").read_text() + files("adcp_reporting_storage.production").joinpath("service_context.sql").read_text() ) await connection.execute( "INSERT INTO adcp_reporting_ownership_archives(archive_schema) VALUES (%s)", diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/__init__.py new file mode 100644 index 000000000..a650be3b8 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/__init__.py @@ -0,0 +1 @@ +"""PostgreSQL notification, status and activity queues.""" diff --git a/src/adcp/reporting/outbox/_activity_pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_activity_pg.py similarity index 97% rename from src/adcp/reporting/outbox/_activity_pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_activity_pg.py index 8549b2858..79b368102 100644 --- a/src/adcp/reporting/outbox/_activity_pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_activity_pg.py @@ -68,7 +68,7 @@ async def _activity_transaction(self) -> AsyncIterator[Any]: raise ReportingNotificationError("activity_store_unavailable") from None async def _activity_fence(self, conn: Any, lease: DeliveryLease) -> datetime | None: - from adcp.reporting.outbox.pg import _DELIVERY_COLUMNS, _delivery, database_now + from adcp_reporting_storage.outbox.pg import _DELIVERY_COLUMNS, _delivery, database_now b = lease.delivery.binding # The projection is an SDK constant; every selector is a bound value. @@ -111,7 +111,7 @@ async def reserve_attempt( async def _reserve_attempt_on( self, conn: Any, lease: DeliveryLease, *, request: ActivityRequest ) -> WebhookAttempt | None: - from adcp.reporting.outbox.pg import database_now + from adcp_reporting_storage.outbox.pg import database_now b = lease.delivery.binding consumer = canonical_consumer(b.principal_id) @@ -185,7 +185,7 @@ async def _next_attempt_on(self, conn: Any, binding: DeliveryBinding) -> int: async def complete_attempt( self, attempt: WebhookAttempt, *, outcome: ActivityOutcome, now: datetime ) -> bool: - from adcp.reporting.outbox.pg import database_now + from adcp_reporting_storage.outbox.pg import database_now ActivityOutcome.__post_init__(outcome) b = attempt.binding @@ -247,7 +247,7 @@ async def list_activity( async def purge_activity( self, *, account_id: str, consumer_id: str, now: datetime, retention_days: int = 30 ) -> int: - from adcp.reporting.outbox.pg import database_now + from adcp_reporting_storage.outbox.pg import database_now consumer_id = canonical_consumer(consumer_id) retention_cutoff(now, retention_days) diff --git a/src/adcp/reporting/outbox/_schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_schema.py similarity index 98% rename from src/adcp/reporting/outbox/_schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_schema.py index b2709cba3..86276fd36 100644 --- a/src/adcp/reporting/outbox/_schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/_schema.py @@ -17,10 +17,10 @@ from adcp.reporting.ledger.notification_models import ReportingNotificationError REQUIRED_OBJECTS: dict[str, dict[str, Any]] = json.loads( - files("adcp.reporting.outbox").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.outbox").joinpath("required_schema.json").read_text() ) PROVISIONAL_REQUIRED_OBJECTS: dict[str, dict[str, Any]] = json.loads( - files("adcp.reporting.ledger").joinpath("required_provisional_schema.json").read_text() + files("adcp_reporting_storage.ledger").joinpath("required_provisional_schema.json").read_text() ) SCHEMA_CONTRACT: dict[str, str] = { key: value["fingerprint"] for key, value in REQUIRED_OBJECTS.items() diff --git a/src/adcp/reporting/outbox/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/pg.py similarity index 93% rename from src/adcp/reporting/outbox/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/pg.py index 7b798ccad..be74e7831 100644 --- a/src/adcp/reporting/outbox/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/pg.py @@ -24,7 +24,6 @@ dirty_storage, event_storage, ) -from adcp.reporting.outbox._activity_pg import _PgReportingActivity from adcp.reporting.outbox.memory import validate_finish from adcp.reporting.outbox.models import ( DeliveryBinding, @@ -35,6 +34,7 @@ StoredDelivery, WorkState, ) +from adcp_reporting_storage.outbox._activity_pg import _PgReportingActivity if TYPE_CHECKING: from psycopg_pool import AsyncConnectionPool @@ -114,7 +114,7 @@ async def mark_dirty( before: ReportingStatusEvidence | None = None, after: ReportingStatusEvidence | None = None, ) -> None: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore await PgReportingLedgerStore._lock_account(connection, scope.account_id) scope_hash = hashlib.sha256(canonical_json_utf8_v1(asdict(scope))).hexdigest() @@ -183,17 +183,41 @@ def __init__( # Same actionable hint the sibling PG stores raise, so a base-install # adopter is told to add the extra instead of meeting a driver error # from inside the first query. - from adcp.reporting.ledger.pg import _INSTALL_HINT, PG_AVAILABLE + from adcp_reporting_storage.ledger.pg import _INSTALL_HINT, PG_AVAILABLE if not PG_AVAILABLE: raise ImportError(_INSTALL_HINT) self._pool, self._clock = pool, clock async def create_schema(self) -> None: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore await PgReportingLedgerStore(pool=self._pool, notifications=True).create_schema() + async def next_due_account(self, *, delivery: bool, now: datetime) -> str | None: + """Sample one indexed due queue row by database time; never enumerate accounts. + + ``now`` is the worker's instant; a durable queue orders by database time + (or the explicit conformance clock), so it is not consulted here. + """ + # Both identifiers are closed SDK literals. The concrete queue adapter + # selects the matching isolated table on this same actual connection. + table = ( + "reporting_notification_deliveries" if delivery else "reporting_notification_expansions" + ) + async with self._connection() as connection: + at = await database_now(connection, self._clock) + row = await ( + await connection.execute( + f"SELECT account_id FROM {table}" # nosec B608 + " WHERE due_at<=%s AND (state='pending' OR" + " (state='leased' AND lease_expires_at<=%s))" + " ORDER BY due_at,account_id LIMIT 1", + (at, at), + ) + ).fetchone() + return str(row[0]) if row is not None else None + async def list_events(self, *, account_id: str) -> tuple[ReportingDomainEvent, ...]: async with self._connection() as conn: rows = await ( diff --git a/src/adcp/reporting/outbox/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_schema.json similarity index 100% rename from src/adcp/reporting/outbox/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_schema.json diff --git a/src/adcp/reporting/outbox/required_status_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_status_schema.json similarity index 100% rename from src/adcp/reporting/outbox/required_status_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_status_schema.json diff --git a/src/adcp/reporting/outbox/required_status_selector_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_status_selector_schema.json similarity index 100% rename from src/adcp/reporting/outbox/required_status_selector_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/required_status_selector_schema.json diff --git a/src/adcp/reporting/outbox/status_activity_pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_activity_pg.py similarity index 86% rename from src/adcp/reporting/outbox/status_activity_pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_activity_pg.py index 48808dbfb..e03413e12 100644 --- a/src/adcp/reporting/outbox/status_activity_pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_activity_pg.py @@ -5,12 +5,12 @@ from datetime import datetime from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.ledger.pg import PG_AVAILABLE -from adcp.reporting.outbox._activity_pg import _ACTIVITY_COLUMNS, _attempt from adcp.reporting.outbox.activity import WebhookAttempt, activity_limit, retention_cutoff from adcp.reporting.outbox.identity import canonical_consumer -from adcp.reporting.outbox.pg import PgReportingOutbox, database_now -from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox +from adcp_reporting_storage.ledger.pg import PG_AVAILABLE +from adcp_reporting_storage.outbox._activity_pg import _ACTIVITY_COLUMNS, _attempt +from adcp_reporting_storage.outbox.pg import PgReportingOutbox, database_now +from adcp_reporting_storage.outbox.status_pg import PgReportingStatusOutbox class PgReportingActivityUnionStore: @@ -18,7 +18,10 @@ class PgReportingActivityUnionStore: def __init__(self, b_outbox: PgReportingOutbox, status_outbox: PgReportingStatusOutbox) -> None: if not PG_AVAILABLE: - raise ImportError("PgReportingActivityUnionStore requires PostgreSQL; install adcp[pg]") + raise ImportError( + "PgReportingActivityUnionStore requires PostgreSQL;" + " install adcp-reporting-storage[pg]" + ) if ( type(b_outbox) is not PgReportingOutbox or type(status_outbox) is not PgReportingStatusOutbox diff --git a/src/adcp/reporting/outbox/status_pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_pg.py similarity index 92% rename from src/adcp/reporting/outbox/status_pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_pg.py index b5f9077db..63f4e91f3 100644 --- a/src/adcp/reporting/outbox/status_pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_pg.py @@ -13,7 +13,7 @@ from datetime import timedelta from importlib.resources import files from secrets import token_hex -from typing import Any +from typing import TYPE_CHECKING, Any from pydantic import TypeAdapter @@ -27,7 +27,6 @@ decode_status_scope, event_storage, ) -from adcp.reporting.ledger.pg import PG_AVAILABLE, PgReportingLedgerStore from adcp.reporting.ledger.status_projection import ( ReportingStatusSnapshot, StatusProjectionInput, @@ -35,13 +34,8 @@ projection_scopes, with_replay_lifecycles, ) -from adcp.reporting.ledger.status_snapshot import ( - persist_replay_lifecycles_on, - settle_snapshot_on, - snapshot_from_storage, -) +from adcp.reporting.ledger.status_snapshot import snapshot_from_storage from adcp.reporting.outbox.models import DeliveryBinding -from adcp.reporting.outbox.pg import PgReportingOutbox, database_now from adcp.reporting.outbox.status import ( StatusCheckpoint, StatusDueLease, @@ -50,6 +44,12 @@ escalation_identity, settled_replay, ) +from adcp_reporting_storage.ledger.pg import PG_AVAILABLE, PgReportingLedgerStore +from adcp_reporting_storage.ledger.status_snapshot import ( + persist_replay_lifecycles_on, + settle_snapshot_on, +) +from adcp_reporting_storage.outbox.pg import PgReportingOutbox, database_now _KEY = ( "account_id, consumer_namespace, delivery_config_id, version, scope_kind, obligation_namespace" @@ -63,6 +63,9 @@ " lease_token, lease_expires_at, selector_semantics_version, selector_writer_floor, initialized" ) +if TYPE_CHECKING: + from adcp.reporting.outbox.worker import ReportingNotificationWorker + @deferred_adapter def _lifecycle_adapter() -> TypeAdapter[ReportingIssueLifecycle]: @@ -201,24 +204,58 @@ def __init__( escalation: ReportingDeliveryEscalation | None = None, ) -> None: if not PG_AVAILABLE: - raise ImportError("PgStatusNotificationStore requires PostgreSQL; install adcp[pg]") + raise ImportError( + "PgStatusNotificationStore requires PostgreSQL; install adcp-reporting-storage[pg]" + ) if not isinstance(ledger, PgReportingLedgerStore) or not ledger._notifications_enabled: raise ReportingNotificationError("status_chain_unready") self.ledger, self.escalation = ledger, escalation self.outbox = PgReportingStatusOutbox(pool=ledger._pool, clock=ledger._clock) + # -- adcp.reporting.outbox.durable.ReportingDurableStatusStore ------------- + + async def status_queue_durable(self, worker: ReportingNotificationWorker) -> bool: + from adcp_reporting_storage.outbox.status_schema import validate_status_schema + + if ( + type(self) is not PgStatusNotificationStore + or type(worker.outbox) is not PgReportingStatusOutbox + or worker.outbox is not self.outbox + or self.ledger._pool is not self.outbox._pool + or not self.ledger._notifications_enabled + or (worker.activity is not None and worker.activity is not worker.outbox) + ): + return False + try: + async with self.ledger._pool.connection() as connection: + await validate_status_schema(connection, activity=worker.activity is not None) + except ReportingNotificationError: + return False + return True + + async def managed_delivery_recorded(self, *, account_id: str) -> bool: + async with self.ledger._pool.connection() as connection: + managed = await ( + await connection.execute( + "SELECT EXISTS(SELECT 1 FROM reporting_reconciliation_records" + " WHERE account_id=%s AND record_kind='destination_binding')", + (account_id,), + ) + ).fetchone() + return bool(managed is not None and managed[0]) + async def create_schema(self) -> None: - from adcp.reporting.outbox.status_schema import validate_status_schema + from adcp_reporting_storage.outbox.status_schema import validate_status_schema async with self.ledger._pool.connection() as connection, connection.transaction(): await self.ledger._create_schema_on(connection) await connection.execute( - files("adcp.reporting.ledger") + files("adcp_reporting_storage.ledger") .joinpath("reporting_status_notifications.sql") .read_text() ) await connection.execute( - files("adcp.reporting.ledger") + files("adcp_reporting_storage.ledger") .joinpath("reporting_status_selector_version.sql") .read_text() ) @@ -345,7 +382,7 @@ async def _enqueue_status_on(self, connection: Any, event: ReportingDomainEvent) await _enqueue_on(connection, event) async def baseline(self, *, account_id: str) -> bool: - from adcp.reporting.outbox.status_schema import validate_status_schema + from adcp_reporting_storage.outbox.status_schema import validate_status_schema async with self._transaction(account_id) as connection: await validate_status_schema(connection) @@ -384,7 +421,7 @@ async def baseline(self, *, account_id: str) -> bool: return True async def baseline_ready(self, *, account_id: str) -> bool: - from adcp.reporting.outbox.status_schema import validate_status_schema + from adcp_reporting_storage.outbox.status_schema import validate_status_schema async with self._transaction(account_id) as connection: await validate_status_schema(connection) diff --git a/src/adcp/reporting/outbox/status_schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_schema.py similarity index 89% rename from src/adcp/reporting/outbox/status_schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_schema.py index a8c8ff010..876abad8f 100644 --- a/src/adcp/reporting/outbox/status_schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/outbox/status_schema.py @@ -7,13 +7,15 @@ from typing import Any from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.outbox._schema import schema_objects, validate_schema +from adcp_reporting_storage.outbox._schema import schema_objects, validate_schema REQUIRED_STATUS_OBJECTS: dict[str, dict[str, Any]] = json.loads( - files("adcp.reporting.outbox").joinpath("required_status_schema.json").read_text() + files("adcp_reporting_storage.outbox").joinpath("required_status_schema.json").read_text() ) REQUIRED_STATUS_SELECTOR_OBJECTS: dict[str, dict[str, Any]] = json.loads( - files("adcp.reporting.outbox").joinpath("required_status_selector_schema.json").read_text() + files("adcp_reporting_storage.outbox") + .joinpath("required_status_selector_schema.json") + .read_text() ) diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/__init__.py new file mode 100644 index 000000000..4fc3fd196 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL production store and its readiness queue.""" diff --git a/src/adcp/reporting/production/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/pg.py similarity index 83% rename from src/adcp/reporting/production/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/production/pg.py index 6d1a13e3e..aa8865eab 100644 --- a/src/adcp/reporting/production/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/pg.py @@ -5,7 +5,7 @@ import asyncio import json import weakref -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from contextvars import ContextVar from datetime import datetime, timedelta @@ -13,14 +13,18 @@ from typing import TYPE_CHECKING, Any from adcp.reporting.ledger._delivery_state import decode_record +from adcp.reporting.ledger._record_rows import _configuration_from_row from adcp.reporting.ledger.delivery_models import ( ReportingDeliveryPrincipal, ReportingDeliveryScope, ReportingDestinationBinding, ) -from adcp.reporting.ledger.models import ReportingConfiguration, ReportingObligationRecord +from adcp.reporting.ledger.models import ( + ReportingConfiguration, + ReportingDeliveryEscalation, + ReportingObligationRecord, +) from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.ledger.pg import _configuration_from_row from adcp.reporting.ledger.producer_progress import acquisition_state, check_next_period from adcp.reporting.ledger.store import LeasedConfiguration, LedgerConflictError, _utc from adcp.reporting.materializer.capture import ReportingMaterializerBoundary @@ -36,10 +40,16 @@ ReportingMaterializerTurn, key_for, ) -from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox +from adcp.reporting.outbox.activity import ActivityRequest, ReportingActivityStore, WebhookAttempt +from adcp.reporting.outbox.models import DeliveryLease from adcp.reporting.production.source_registry import ReportingProductionSourceContext -from adcp.reporting.projection.pg import PgReportingProjectionStore +from adcp.reporting.production.store import ReportingProductionProjection, SavedDeliveryWindow from adcp.reporting.source import ReportingConstituent +from adcp_reporting_storage.outbox.status_pg import PgReportingStatusOutbox +from adcp_reporting_storage.projection.pg import ( + PgReportingProjectionStore, + PgReportingStatusProjection, +) if TYPE_CHECKING: from adcp.reporting.production.service import ReportingProductionSupport @@ -142,7 +152,7 @@ class PgReportingProductionStore(PgReportingProjectionStore): permanently quarantined enqueue. Installation alone admits nothing. """ - _production_support: weakref.ReferenceType[ReportingProductionSupport] | None = None + production_support: weakref.ReferenceType[ReportingProductionSupport] | None = None _production_lease_samples: dict[tuple[str, ...], _ProducerSample] | None = None def _owner(self) -> ReportingProductionSupport: @@ -150,6 +160,160 @@ def _owner(self) -> ReportingProductionSupport: return production_owner(self) + # -- the production store contract (adcp.reporting.production.store) ------ + + @property + def durable(self) -> bool: + return True + + def is_sdk_production_store(self) -> bool: + return type(self) is PgReportingProductionStore + + def status_projection( + self, + *, + consumer_status_enabled: bool, + escalation: ReportingDeliveryEscalation | None, + revision_ownership: bool, + ) -> PgReportingStatusProjection: + return PgReportingStatusProjection( + self, + consumer_status_enabled=consumer_status_enabled, + escalation=escalation, + revision_ownership=revision_ownership, + ) + + def is_sdk_status_projection(self, projection: object) -> bool: + return type(projection) is PgReportingStatusProjection + + def projection_linked(self, projection: ReportingProductionProjection) -> bool: + from adcp_reporting_storage.projection.notifications import PgReportingProjectionOutbox + + return ( + type(projection.outbox) is PgReportingProjectionOutbox + and projection.outbox._pool is self._pool + ) + + def production_outboxes( + self, projection: ReportingProductionProjection + ) -> tuple[Any, Any, Any]: + from adcp_reporting_storage.outbox.pg import PgReportingOutbox + + if type(self) is not PgReportingProductionStore: + raise ReportingNotificationError("notification_chain_unready") + return ( + PgReportingOutbox(pool=self._pool, clock=self._clock), + projection.outbox, + PgReportingProductionOutbox(pool=self._pool, clock=self._clock), + ) + + def notification_workers_linked(self, workers: Sequence[Any]) -> bool: + from adcp_reporting_storage.outbox.pg import PgReportingOutbox + from adcp_reporting_storage.projection.notifications import PgReportingProjectionOutbox + + expected = (PgReportingOutbox, PgReportingProjectionOutbox, PgReportingProductionOutbox) + return ( + all(getattr(w.outbox, "_pool", None) is self._pool for w in workers) + and tuple(type(w.outbox) for w in workers) == expected + ) + + async def validate_production_schema( + self, *, notifications: bool, service_context: bool + ) -> None: + from adcp_reporting_storage.production.schema import validate_production_schema + + async with self._pool.connection() as connection: + await validate_production_schema( + connection, notifications=notifications, service_context=service_context + ) + + async def read_delivery_window( + self, key: tuple[str, str], *, now: datetime + ) -> tuple[SavedDeliveryWindow | None, datetime]: + from adcp_reporting_storage.outbox.pg import database_now + + try: + async with self._connection() as connection, connection.transaction(): + saved = await ( + await connection.execute( + "SELECT queue,body_sha256,started_at,expires_at" + " FROM reporting_production_delivery_windows" + " WHERE account_id=%s AND idempotency_key=%s", + key, + ) + ).fetchone() + moment = await database_now(connection, self._clock) + except ReportingNotificationError: + raise + except Exception: + raise ReportingNotificationError("notification_retry_unready") from None + return (tuple(saved) if saved is not None else None), moment + + async def reserve_delivery_attempt( + self, + activity: ReportingActivityStore, + lease: DeliveryLease, + *, + request: ActivityRequest, + now: datetime, + queue: str, + expected: tuple[str, str], + horizon_seconds: int, + ) -> tuple[WebhookAttempt | None, datetime | None, bool]: + from adcp.reporting.production.delivery_window import inspect_window + from adcp_reporting_storage.outbox.pg import PgReportingOutbox, database_now + + binding = lease.delivery.binding + key = (binding.account_id, binding.idempotency_key) + if not isinstance(activity, PgReportingOutbox) or activity._pool is not self._pool: + raise ReportingNotificationError("notification_retry_unready") + deadline: datetime | None = None + try: + # The activity participant supplies its exact queue adapter and + # connection. Take the inherited account lock before its row lock. + async with activity._activity_transaction() as connection: + await self._lock_account(connection, binding.account_id) + moment = await database_now(connection, self._clock) + saved = await ( + await connection.execute( + "SELECT queue,body_sha256,started_at,expires_at" + " FROM reporting_production_delivery_windows" + " WHERE account_id=%s AND idempotency_key=%s", + key, + ) + ).fetchone() + deadline, expired = inspect_window(saved, expected, moment) + if expired: + return None, deadline, True + attempt = await activity._reserve_attempt_on(connection, lease, request=request) + if attempt is None: + return None, deadline, False + deadline, expired = inspect_window(saved, expected, attempt.fired_at) + if expired: + # Time can cross the deadline while reserving an ordinal. + # Roll back that provisional head and attempt, too. + raise ReportingNotificationError("notification_retry_expired") + await connection.execute( + "INSERT INTO reporting_production_delivery_windows" + " (account_id,idempotency_key,queue,body_sha256,started_at,expires_at)" + " VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING", + ( + *key, + *expected, + attempt.fired_at, + attempt.fired_at + timedelta(seconds=horizon_seconds), + ), + ) + return ( + attempt, + deadline or attempt.fired_at + timedelta(seconds=horizon_seconds), + False, + ) + except ReportingNotificationError as error: + if error.code == "notification_retry_expired": + return None, deadline, True + raise + @asynccontextmanager async def _connection(self) -> AsyncIterator[Any]: admission = _ADMISSION_CONNECTION.get() @@ -629,7 +793,7 @@ async def _lease_epoch(self, lease: ReportingMaterializerLease) -> AsyncIterator async def create_schema(self) -> None: async with self._connection() as connection, connection.transaction(): await self._create_schema_on(connection) - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") for name in ( "reporting_materializer.sql", "reporting_receipt_ingestion.sql", @@ -643,7 +807,9 @@ async def create_schema(self) -> None: ): await connection.execute(root.joinpath(name).read_text()) await connection.execute( - files("adcp.reporting.production").joinpath("service_context.sql").read_text() + files("adcp_reporting_storage.production") + .joinpath("service_context.sql") + .read_text() ) async def materializer_ready(self) -> bool: @@ -653,7 +819,7 @@ async def materializer_ready(self) -> bool: owner._assert_components() return True - async def _activate_production(self, *, account_id: str) -> bool: + async def activate_production(self, *, account_id: str) -> bool: owner = self._owner() await self.materializer_ready() async with self._connection() as connection, connection.transaction(): diff --git a/src/adcp/reporting/production/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/required_schema.json similarity index 100% rename from src/adcp/reporting/production/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/production/required_schema.json diff --git a/src/adcp/reporting/production/schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/schema.py similarity index 76% rename from src/adcp/reporting/production/schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/production/schema.py index 7212a3ffd..228d541d3 100644 --- a/src/adcp/reporting/production/schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/schema.py @@ -7,8 +7,8 @@ from typing import Any from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.outbox._schema import schema_objects -from adcp.reporting.projection.schema import validate_projection_schema +from adcp_reporting_storage.outbox._schema import schema_objects +from adcp_reporting_storage.projection.schema import validate_projection_schema async def validate_production_schema( @@ -16,12 +16,12 @@ async def validate_production_schema( ) -> None: try: required = json.loads( - files("adcp.reporting.production").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.production").joinpath("required_schema.json").read_text() ) if service_context: required.update( json.loads( - files("adcp.reporting.production") + files("adcp_reporting_storage.production") .joinpath("service_context_schema.json") .read_text() ) diff --git a/src/adcp/reporting/production/service_context.sql b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/service_context.sql similarity index 100% rename from src/adcp/reporting/production/service_context.sql rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/production/service_context.sql diff --git a/src/adcp/reporting/production/service_context_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/production/service_context_schema.json similarity index 100% rename from src/adcp/reporting/production/service_context_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/production/service_context_schema.json diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/__init__.py new file mode 100644 index 000000000..92d380ffb --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL projection store, status projection and projection queue.""" diff --git a/src/adcp/reporting/projection/notifications.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/notifications.py similarity index 89% rename from src/adcp/reporting/projection/notifications.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/notifications.py index 8349cd2a8..ba2a695f3 100644 --- a/src/adcp/reporting/projection/notifications.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/notifications.py @@ -6,7 +6,7 @@ from contextlib import asynccontextmanager from typing import Any -from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox +from adcp_reporting_storage.outbox.status_pg import PgReportingStatusOutbox class _ProjectionQueueConnection: @@ -36,6 +36,6 @@ async def _connection(self) -> AsyncIterator[Any]: yield _ProjectionQueueConnection(connection) async def create_schema(self) -> None: - from adcp.reporting.projection.pg import PgReportingProjectionStore + from adcp_reporting_storage.projection.pg import PgReportingProjectionStore await PgReportingProjectionStore(pool=self._pool, notifications=True).create_schema() diff --git a/src/adcp/reporting/projection/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/pg.py similarity index 96% rename from src/adcp/reporting/projection/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/pg.py index b24e723eb..577af64d6 100644 --- a/src/adcp/reporting/projection/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/pg.py @@ -10,7 +10,6 @@ from importlib.resources import files from typing import Any -from adcp.reporting.feed.pg import PgReportingFeedStore, _CapturedFeed, _PreparedFeed from adcp.reporting.feed.snapshot import StoredFeedSnapshot from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal from adcp.reporting.ledger.models import ReportingDeliveryEscalation @@ -19,17 +18,12 @@ ReportingNotificationError, ) from adcp.reporting.ledger.status_projection import with_replay_lifecycles -from adcp.reporting.ledger.status_snapshot import persist_replay_lifecycles_on -from adcp.reporting.outbox.pg import database_now from adcp.reporting.outbox.status import ( StatusDueLease, StatusTurn, escalation_identity, settled_replay, ) -from adcp.reporting.outbox.status_pg import _CHECKPOINT as _LEGACY_CHECKPOINT -from adcp.reporting.outbox.status_pg import PgStatusNotificationStore, _enqueue_on -from adcp.reporting.outbox.status_pg import _checkpoint as _legacy_checkpoint from adcp.reporting.projection.capture import ( ReportingProjectionInput, decode_projection_input, @@ -42,11 +36,17 @@ decode_checkpoint, project_boundary, ) -from adcp.reporting.projection.notifications import ( +from adcp_reporting_storage.feed.pg import PgReportingFeedStore, _CapturedFeed, _PreparedFeed +from adcp_reporting_storage.ledger.status_snapshot import persist_replay_lifecycles_on +from adcp_reporting_storage.outbox.pg import database_now +from adcp_reporting_storage.outbox.status_pg import _CHECKPOINT as _LEGACY_CHECKPOINT +from adcp_reporting_storage.outbox.status_pg import PgStatusNotificationStore, _enqueue_on +from adcp_reporting_storage.outbox.status_pg import _checkpoint as _legacy_checkpoint +from adcp_reporting_storage.projection.notifications import ( PgReportingProjectionOutbox, _ProjectionQueueConnection, ) -from adcp.reporting.projection.schema import validate_projection_schema +from adcp_reporting_storage.projection.schema import validate_projection_schema class _ProjectionFeedConnection: @@ -64,13 +64,15 @@ async def execute(self, query: str, params: Any = None) -> Any: class PgReportingProjectionStore(PgReportingFeedStore): """Optional v2 store. Installation alone neither activates tiers nor sends readiness.""" - _projection_read_policy: dict[str, Any] - _projection_read_escalation: ReportingDeliveryEscalation | None + # Replaced (never mutated) by the status projection that reads this store; + # present from construction so the production store protocol matches. + projection_read_policy: dict[str, Any] = {} + projection_read_escalation: ReportingDeliveryEscalation | None = None async def create_schema(self) -> None: async with self._connection() as connection, connection.transaction(): await self._create_schema_on(connection) - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") for name in ( "reporting_materializer.sql", "reporting_receipt_ingestion.sql", @@ -196,8 +198,8 @@ def __init__( revision_ownership, ) self.outbox = PgReportingProjectionOutbox(pool=ledger._pool, clock=ledger._clock) - ledger._projection_read_policy = self.policy - ledger._projection_read_escalation = escalation + ledger.projection_read_policy = self.policy + ledger.projection_read_escalation = escalation async def _enqueue_status_on(self, connection: Any, event: ReportingDomainEvent) -> None: await _enqueue_on(_ProjectionQueueConnection(connection), event) diff --git a/src/adcp/reporting/projection/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/required_schema.json similarity index 100% rename from src/adcp/reporting/projection/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/required_schema.json diff --git a/src/adcp/reporting/projection/schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/schema.py similarity index 72% rename from src/adcp/reporting/projection/schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/schema.py index 9b46a0592..e4bb3c4a0 100644 --- a/src/adcp/reporting/projection/schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/projection/schema.py @@ -6,16 +6,16 @@ from importlib.resources import files from typing import Any -from adcp.reporting.feed.schema import validate_feed_schema from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.outbox._schema import schema_objects -from adcp.reporting.outbox.status_schema import validate_status_schema +from adcp_reporting_storage.feed.schema import validate_feed_schema +from adcp_reporting_storage.outbox._schema import schema_objects +from adcp_reporting_storage.outbox.status_schema import validate_status_schema async def validate_projection_schema(connection: Any, *, notifications: bool = False) -> None: try: required = json.loads( - files("adcp.reporting.projection").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.projection").joinpath("required_schema.json").read_text() ) actual = await schema_objects(connection) if not required or any(actual.get(k) != v for k, v in required.items()): diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/py.typed b/packages/adcp-reporting-storage/src/adcp_reporting_storage/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/__init__.py new file mode 100644 index 000000000..c5ba36036 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL receipt store.""" diff --git a/src/adcp/reporting/receipts/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/pg.py similarity index 97% rename from src/adcp/reporting/receipts/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/pg.py index b219b3ac6..57d963002 100644 --- a/src/adcp/reporting/receipts/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/pg.py @@ -11,6 +11,12 @@ from adcp.reporting.canonical_json import canonical_json_sha256_v1 from adcp.reporting.ledger._delivery_state import RecordT +from adcp.reporting.ledger._record_rows import ( + _OBLIGATION_COLUMNS, + _REVISION_COLUMNS, + _obligation_from_row, + _revision_from_row, +) from adcp.reporting.ledger.delivery_models import ( ReportingAdjustmentReceiptRecord, ReportingDeliveryPrincipal, @@ -18,17 +24,8 @@ ReportingReceiptRecord, ReportingRevisionReceiptRecord, ) -from adcp.reporting.ledger.pg import ( - _OBLIGATION_COLUMNS, - _REVISION_COLUMNS, - _json, - _obligation_from_row, - _revision_from_row, -) -from adcp.reporting.ledger.status_snapshot import settle_snapshot_on from adcp.reporting.ledger.store import LedgerConflictError from adcp.reporting.materializer.capture import private_snapshot -from adcp.reporting.materializer.pg import PgReportingMaterializerStore, _now from adcp.reporting.receipts._diagnostics import _Boundary, _storage_failure from adcp.reporting.receipts.capture import ReportingReceiptBoundary, decode_receipt_boundary from adcp.reporting.receipts.errors import ReportingReceiptError @@ -38,7 +35,6 @@ receipt_record, success_result, ) -from adcp.reporting.receipts.schema import validate_receipt_schema from adcp.reporting.receipts.wire import ( ReceiptBatch, ReceiptKind, @@ -46,6 +42,10 @@ validate_receipt_results, ) from adcp.server.helpers import inject_context +from adcp_reporting_storage.ledger.pg import _json +from adcp_reporting_storage.ledger.status_snapshot import settle_snapshot_on +from adcp_reporting_storage.materializer.pg import PgReportingMaterializerStore, _now +from adcp_reporting_storage.receipts.schema import validate_receipt_schema _P = ParamSpec("_P") _R = TypeVar("_R") @@ -86,7 +86,7 @@ class PgReportingReceiptStore(PgReportingMaterializerStore): async def create_schema(self) -> None: async with self._connection() as connection, connection.transaction(): await self._create_schema_on(connection) - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") await connection.execute(root.joinpath("reporting_materializer.sql").read_text()) await connection.execute(root.joinpath("reporting_receipt_ingestion.sql").read_text()) diff --git a/src/adcp/reporting/receipts/required_schema.json b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/required_schema.json similarity index 100% rename from src/adcp/reporting/receipts/required_schema.json rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/required_schema.json diff --git a/src/adcp/reporting/receipts/schema.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/schema.py similarity index 76% rename from src/adcp/reporting/receipts/schema.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/schema.py index cb874c93e..7991e76b7 100644 --- a/src/adcp/reporting/receipts/schema.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/receipts/schema.py @@ -6,16 +6,16 @@ from importlib.resources import files from typing import Any -from adcp.reporting.materializer.schema import validate_materializer_schema -from adcp.reporting.outbox._schema import schema_objects from adcp.reporting.receipts.errors import ReportingReceiptError +from adcp_reporting_storage.materializer.schema import validate_materializer_schema +from adcp_reporting_storage.outbox._schema import schema_objects async def validate_receipt_schema(connection: Any, *, notifications: bool = False) -> None: ready = False try: required = json.loads( - files("adcp.reporting.receipts").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.receipts").joinpath("required_schema.json").read_text() ) installed = await schema_objects(connection) if required and all(installed.get(key) == expected for key, expected in required.items()): diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py new file mode 100644 index 000000000..a8fa46d38 --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/service.py @@ -0,0 +1,58 @@ +"""The adapter-first Reliable Reporting service over a borrowed PostgreSQL pool.""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +from typing import Any + +from adcp.reporting.service import ( + ReliableReportingService, + ReportingCallerResolver, + ReportingContextResolver, +) +from adcp.reporting.service_production import ReportingProductionOptions + +__all__ = ["postgres_reliable_reporting_service"] + + +def postgres_reliable_reporting_service( + *, + pool: Any, + account_context: ReportingContextResolver, + caller_resolver: ReportingCallerResolver | None = None, + clock: Callable[[], datetime] | None = None, + production: ReportingProductionOptions | None = None, + **kwargs: Any, +) -> ReliableReportingService: + """Create durable ledger, staging and seal stores over a borrowed pool. + + ``production`` additionally composes the managed pipeline, receipts and + optional signed notifications when ``install`` freezes registered sources. + Startup installs all SDK-owned schemas. Explicit source-store overrides + remain the adopter's responsibility. No pool is opened or closed here. + + This is what ``ReliableReportingService.postgres(...)`` was before the + storage implementations moved to this distribution. + """ + from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.production.pg import PgReportingProductionStore + + store = ( + PgReportingProductionStore(pool=pool, clock=clock, notifications=production.notifications) + if production is not None + else PgReportingLedgerStore(pool=pool, clock=clock) + ) + staging = PgReportingStagingStore(pool=pool) + return ReliableReportingService( + store=store, + account_context=account_context, + caller_resolver=caller_resolver, + clock=clock, + production=production, + staging=staging, + seals=PgReportingSealStore(pool=pool), + initialize_source_storage=staging.create_schema, + **kwargs, + ) diff --git a/packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/__init__.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/__init__.py new file mode 100644 index 000000000..eeb4aac1d --- /dev/null +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/__init__.py @@ -0,0 +1 @@ +"""The PostgreSQL buyer submission intent store.""" diff --git a/src/adcp/reporting/submissions/pg.py b/packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/pg.py similarity index 99% rename from src/adcp/reporting/submissions/pg.py rename to packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/pg.py index cd3147f6d..05d357472 100644 --- a/src/adcp/reporting/submissions/pg.py +++ b/packages/adcp-reporting-storage/src/adcp_reporting_storage/submissions/pg.py @@ -2,7 +2,7 @@ The application owns the pool and its lifecycle. ``create_schema`` installs only the isolated buyer tables, without modifying seller schemas or existing APIs. -Importing this module does not require psycopg; actual use requires adcp[pg]. +Importing this module does not require psycopg; actual use requires adcp-reporting-storage[pg]. """ from __future__ import annotations @@ -100,7 +100,7 @@ async def create_schema(self) -> None: async with self._pool.connection() as connection, connection.transaction(): await connection.execute("SELECT pg_advisory_xact_lock(%s)", (712071172,)) sql = ( - files("adcp.reporting.ledger") + files("adcp_reporting_storage.ledger") .joinpath("reporting_buyer_submissions.sql") .read_text() ) diff --git a/pyproject.toml b/pyproject.toml index 8317e6be6..cec53208f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,23 +198,16 @@ pg = [ # PostgreSQL-backed adcp.signing.PgReplayStore, # adcp.decisioning.PostgresTaskRegistry (durable HITL task state), and # future PgIdempotencyBackend. psycopg3 ships both sync + async pool - # interfaces so the single dep serves all three use cases. + # interfaces so the single dep serves all three use cases. The reporting + # stores are ``adcp-reporting-storage[pg]``. "psycopg[binary]>=3.1.0", "psycopg-pool>=3.2.0", ] -reporting-gcs = [ - # Row storage on Google Cloud Storage. The provider takes a client you build, - # with keyless credentials; it never imports the library itself. - "google-cloud-storage>=2.18", -] -reporting-s3 = [ - # Row storage on Amazon S3 or an S3-compatible store (needs bucket versioning). - "boto3>=1.35", -] -reporting-azure = [ - # Row storage on Azure Blob Storage (needs blob versioning on the account). - "azure-storage-blob>=12.20", -] +# The reporting row-storage providers (``reporting-gcs``, ``reporting-s3``, +# ``reporting-azure``) moved with the PostgreSQL reporting stores to the +# separately versioned ``adcp-reporting-storage`` distribution +# (packages/adcp-reporting-storage); install its ``[gcs]``/``[s3]``/``[azure]`` +# extras instead. observability = [ # API-only library instrumentation: without an application-configured SDK # provider/exporter, OpenTelemetry remains a non-recording no-op. @@ -244,16 +237,7 @@ adcp = [ "notification_outbox.sql", "signing/pg/*.sql", "decisioning/pg/*.sql", - "reporting/ledger/*.sql", - "reporting/ledger/*.json", - "reporting/outbox/*.json", "reporting/materializer/assets/*.json", - "reporting/materializer/*.json", - "reporting/receipts/*.json", - "reporting/feed/*.json", - "reporting/projection/*.json", - "reporting/production/*.json", - "reporting/production/*.sql", # PREVIEW: vendored sync_reporting_status schemas. They are the runtime # validator for the wire conditionals codegen cannot express, so the wheel # must carry them. Removed with the rest of _preview/ at rc.2. @@ -435,8 +419,18 @@ directory = "htmlcov" exclude_dirs = ["tests", "scripts"] skips = ["B101"] # Allow assert in code (we're not using -O optimization) +[tool.uv.workspace] +members = ["packages/adcp-reporting-storage"] + +[tool.uv.sources] +adcp-reporting-storage = { workspace = true } + [dependency-groups] dev = [ + # The storage distribution, editable, with every provider extra, so the + # shared test suite and type checks see both distributions at once. pip + # users install it explicitly: ``make install-dev``. + "adcp-reporting-storage[pg,gcs,s3,azure]", "datamodel-code-generator==0.64.0", "pre-commit>=4.4.0", "types-protobuf>=7.34.1.20260408", diff --git a/release-please-config.json b/release-please-config.json index 81a7d6407..8b69b51a2 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,14 +9,45 @@ "prerelease": true, "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, - "include-component-in-tag": false + "include-component-in-tag": false, + "exclude-paths": [ + "packages/adcp-reporting-storage" + ] + }, + "packages/adcp-reporting-storage": { + "release-type": "python", + "package-name": "adcp-reporting-storage", + "component": "adcp-reporting-storage", + "changelog-path": "CHANGELOG.md", + "versioning": "prerelease", + "prerelease-type": "beta", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "include-component-in-tag": true } }, "changelog-sections": [ - {"type": "feat", "section": "Features"}, - {"type": "fix", "section": "Bug Fixes"}, - {"type": "perf", "section": "Performance Improvements"}, - {"type": "revert", "section": "Reverts"}, - {"type": "deps", "section": "Dependencies"} - ] + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "deps", + "section": "Dependencies" + } + ], + "separate-pull-requests": true } diff --git a/scripts/ci/ci_scope.py b/scripts/ci/ci_scope.py index 0e07d8f2f..1b5098348 100644 --- a/scripts/ci/ci_scope.py +++ b/scripts/ci/ci_scope.py @@ -80,6 +80,7 @@ def scope_for_paths(paths: list[str]) -> Scope: elif path.startswith( ( "src/adcp/reporting/", + "packages/adcp-reporting-storage/src/", "src/adcp/decisioning/", "src/adcp/signing/", "src/adcp/server/", diff --git a/scripts/ci/reporting_interop/python_core_server.py b/scripts/ci/reporting_interop/python_core_server.py index 8089affb3..a7f684295 100644 --- a/scripts/ci/reporting_interop/python_core_server.py +++ b/scripts/ci/reporting_interop/python_core_server.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse +import importlib.util import os from dataclasses import fields from datetime import datetime, timedelta, timezone @@ -17,7 +18,6 @@ from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( - PgReportingLedgerStore, ReportingConfiguration, ReportingDefinitionBinding, ReportingObligationRecord, @@ -28,6 +28,12 @@ derive_period, revision_content_sha256, ) + +if importlib.util.find_spec("adcp.reporting.ledger.pg") is not None: + # Published wheels before the storage split (#1510) carry the store in adcp. + from adcp.reporting.ledger import PgReportingLedgerStore +else: + from adcp_reporting_storage import PgReportingLedgerStore from adcp.reporting.ledger.status_server import ReportingStatusNotificationHandler from adcp.server import serve from adcp.server.auth import BearerTokenAuth, Principal, auth_context_factory diff --git a/scripts/export_resolution.py b/scripts/export_resolution.py index 702a9e994..efb4e362a 100644 --- a/scripts/export_resolution.py +++ b/scripts/export_resolution.py @@ -22,9 +22,9 @@ #: Namespaces the snapshot covers: the two public surfaces an adopter imports -#: from, the consolidated generated namespace behind them, and every generated -#: domain module, plus the reporting and ledger public surfaces. Built on first -#: use so this module imports nothing from +#: from, the consolidated generated namespace behind them, every generated +#: domain module, the reporting and ledger surfaces (#1490), and the storage +#: distribution's facade. Built on first use so this module imports nothing from #: ``adcp`` until asked. #: #: ``error_details`` is deliberately absent. Every name it carries is a class @@ -42,6 +42,7 @@ def snapshot_modules() -> tuple[tuple[str, str], ...]: ("adcp.types._generated", "adcp.types._generated"), ("adcp.reporting", "adcp.reporting"), ("adcp.reporting.ledger", "adcp.reporting.ledger"), + ("adcp_reporting_storage", "adcp_reporting_storage"), *( (f"adcp.types.domains.{domain}", f"adcp.types.domains.{domain}") for domain in adcp.types.domains.DOMAINS diff --git a/scripts/generate_row_storage_catalog.py b/scripts/generate_row_storage_catalog.py index d4dae587c..f511949a2 100644 --- a/scripts/generate_row_storage_catalog.py +++ b/scripts/generate_row_storage_catalog.py @@ -20,14 +20,16 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) +sys.path.insert(0, str(ROOT / "packages/adcp-reporting-storage/src")) -from adcp.reporting.ledger.row_storage import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage import ( # noqa: E402 REPORTING_ROW_STORAGE_DDL_PATH, REPORTING_ROW_STORAGE_TABLES, ) -from adcp.reporting.outbox._schema import _catalog_objects # noqa: E402 +from adcp_reporting_storage.outbox._schema import _catalog_objects # noqa: E402 -TARGET = ROOT / "src/adcp/reporting/ledger/required_row_storage_schema.json" +STORAGE_SRC = ROOT / "packages/adcp-reporting-storage/src/adcp_reporting_storage" +TARGET = STORAGE_SRC / "ledger/required_row_storage_schema.json" async def generate(url: str) -> dict[str, dict[str, object]]: diff --git a/scripts/normalize_pyproject_prerelease.py b/scripts/normalize_pyproject_prerelease.py index 742e0cb81..d076fba7f 100644 --- a/scripts/normalize_pyproject_prerelease.py +++ b/scripts/normalize_pyproject_prerelease.py @@ -28,9 +28,13 @@ "rc": "rc", } _REPOSITORY = "adcontextprotocol/adcp-client-python" +#: Release Please branch -> the pyproject.toml that branch's component versions. _RELEASE_BRANCHES = { - "release-please--branches--main", - "release-please--branches--main--components--adcp", + "release-please--branches--main": "pyproject.toml", + "release-please--branches--main--components--adcp": "pyproject.toml", + "release-please--branches--main--components--adcp-reporting-storage": ( + "packages/adcp-reporting-storage/pyproject.toml" + ), } @@ -104,9 +108,15 @@ def _github(path: str, token: str, *, body: dict[str, Any] | None = None) -> Any def normalize_release_prs(prs: list[dict[str, Any]], token: str) -> None: - """Normalize the one Release Please PR with the App credential that opened it.""" - if len(prs) > 1: - raise ValueError("expected at most one release PR") + """Normalize each component's Release Please PR with the App credential that opened it. + + Release Please opens one PR per component (``separate-pull-requests``), so a + run may propose both the ``adcp`` and the ``adcp-reporting-storage`` PR; each + is normalized in its own ``pyproject.toml``. + """ + if len(prs) > len(_RELEASE_BRANCHES): + raise ValueError("more release PRs than release components") + seen: set[str] = set() for proposed in prs: number = int(proposed["number"]) pr = _github(f"pulls/{number}", token) @@ -116,16 +126,17 @@ def normalize_release_prs(prs: list[dict[str, Any]], token: str) -> None: or pr["base"]["repo"]["full_name"] != _REPOSITORY or pr["head"]["repo"]["full_name"] != _REPOSITORY or branch not in _RELEASE_BRANCHES + or branch in seen ): raise ValueError("unexpected release PR identity") - original = _github( - f"contents/pyproject.toml?ref={urllib.parse.quote(branch, safe='')}", token - ) + seen.add(branch) + pyproject = _RELEASE_BRANCHES[branch] + original = _github(f"contents/{pyproject}?ref={urllib.parse.quote(branch, safe='')}", token) contents = base64.b64decode(original["content"], validate=False).decode() normalized = normalize_pyproject_text(contents) if normalized != contents: _github( - "contents/pyproject.toml", + f"contents/{pyproject}", token, body={ "message": "chore: normalize prerelease version to PEP 440", diff --git a/scripts/regenerate_row_storage_interop_fixtures.py b/scripts/regenerate_row_storage_interop_fixtures.py new file mode 100644 index 000000000..faf83c668 --- /dev/null +++ b/scripts/regenerate_row_storage_interop_fixtures.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Refresh ``tests/fixtures/reporting-row-storage-interop-v1/`` from a live run. + +Runs the same steps as the live cross-SDK test +(``tests/conformance/reporting/test_reporting_row_storage_interop_pg.py``): +the JavaScript SDK writes the golden row sets into one PostgreSQL schema and +one filesystem bucket, this SDK verifies them and writes its own, and what +each writer left behind is recorded for the replay tests. + +Usage:: + + ADCP_JS_SDK_DIR=/path/to/adcp-client ADCP_PG_TEST_URL=postgresql://... \\ + python scripts/regenerate_row_storage_interop_fixtures.py + +Needs Node and a built adcp-client checkout (``npm ci && npm run build && +npm run build --workspace packages/reporting-storage``). The fixtures are +written one item per line (#1491); commit the diff with the change that +motivated it. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from tests.conformance.reporting import _row_storage_interop as interop # noqa: E402 +from tests.conformance.reporting._generation_support import isolated_reporting_pool # noqa: E402 +from tests.conformance.reporting.test_reporting_row_storage_interop_pg import ( # noqa: E402 + run_js, + setup, +) + + +async def regenerate(js_sdk: str, workspace: Path) -> None: + root = workspace / "bucket" + async with isolated_reporting_pool() as pool: + _, storage = await setup(pool, root) + async with pool.connection() as connection: + schema = (await (await connection.execute("SELECT current_schema()")).fetchone())[0] + environment = { + "ADCP_JS_SDK_DIR": js_sdk, + "ADCP_PG_URL": os.environ["ADCP_PG_TEST_URL"], + "ADCP_PG_SCHEMA": schema, + "ADCP_ROWS_ROOT": str(root), + "ADCP_ROWS_NAMESPACE": interop.NAMESPACE, + } + js_out = workspace / "js-written.json" + await run_js("write", "--out", str(js_out), environment=environment) + js_written = json.loads(js_out.read_text()) + assert await interop.python_verify(storage, pool, js_written) == len(js_written) + python_written = await interop.python_write(storage, pool) + py_out = workspace / "python-written.json" + py_out.write_text(json.dumps(python_written)) + await run_js("read", "--in", str(py_out), environment=environment) + for name, written in (("js-written", js_written), ("python-written", python_written)): + state = await interop.dump_state(pool, root, [item["row_set_id"] for item in written]) + path = interop.INTEROP_DIR / f"{name}.json" + interop.write_fixture(path, written, state) + print(f"wrote {path.relative_to(ROOT)} ({len(written)} row sets)") + + +def main() -> int: + js_sdk = os.environ.get("ADCP_JS_SDK_DIR") + if not js_sdk or not os.environ.get("ADCP_PG_TEST_URL"): + raise SystemExit("set ADCP_JS_SDK_DIR (a built adcp-client checkout) and ADCP_PG_TEST_URL") + with tempfile.TemporaryDirectory(prefix="adcp-row-storage-interop-") as workspace: + asyncio.run(regenerate(js_sdk, Path(workspace))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/adcp/reporting/feed/__init__.py b/src/adcp/reporting/feed/__init__.py index b623d4394..7a53bc664 100644 --- a/src/adcp/reporting/feed/__init__.py +++ b/src/adcp/reporting/feed/__init__.py @@ -1,29 +1,15 @@ -"""Frozen authorized reporting feeds; PostgreSQL remains an optional lazy import.""" - -from typing import TYPE_CHECKING, Any +"""Frozen authorized reporting feeds; the PostgreSQL store is in ``adcp_reporting_storage``.""" from adcp.reporting.feed.errors import FeedErrorCode, ReportingFeedError from adcp.reporting.feed.memory import InMemoryReportingFeedStore from adcp.reporting.feed.snapshot import ReportingFeedRecord, ReportingFeedSnapshot from adcp.reporting.feed.store import ReportingFeedStore -if TYPE_CHECKING: - from adcp.reporting.feed.pg import PgReportingFeedStore - __all__ = [ "FeedErrorCode", "InMemoryReportingFeedStore", - "PgReportingFeedStore", "ReportingFeedError", "ReportingFeedRecord", "ReportingFeedSnapshot", "ReportingFeedStore", ] - - -def __getattr__(name: str) -> Any: - if name == "PgReportingFeedStore": - from adcp.reporting.feed.pg import PgReportingFeedStore - - return PgReportingFeedStore - raise AttributeError(name) diff --git a/src/adcp/reporting/inline_storage.py b/src/adcp/reporting/inline_storage.py index f32d15cfa..5f06d2f5b 100644 --- a/src/adcp/reporting/inline_storage.py +++ b/src/adcp/reporting/inline_storage.py @@ -46,18 +46,14 @@ import asyncio import hashlib import re -from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Mapping -from contextlib import asynccontextmanager +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from functools import wraps -from importlib.resources import files -from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, ParamSpec, TypeVar +from typing import TYPE_CHECKING, Any, Generic, Literal, ParamSpec, TypeVar from anyio import CancelScope, current_effective_deadline -from adcp.reporting._inline_storage_schema import REQUIRED_OBJECTS from adcp.reporting.inline_source import SealedSlice -from adcp.reporting.outbox._schema import schema_objects from adcp.reporting.source import ( SOURCE_BATCH_MANIFEST_MAX_BYTES_V1, SourceBatchManifestReferenceV1, @@ -65,13 +61,11 @@ ) if TYPE_CHECKING: - from psycopg_pool import AsyncConnectionPool + pass __all__ = [ "INLINE_STAGING_MAX_BYTES", "InlineStorageError", - "PgReportingSealStore", - "PgReportingStagingStore", ] INLINE_STAGING_MAX_BYTES = 16_777_216 @@ -330,264 +324,3 @@ def _seal(account_id: str, key: str, sealed: SealedSlice, code: _Code) -> Sealed except Exception: failure = InlineStorageError(code) raise failure - - -class _PgStorage: - is_durable: ClassVar[bool] = True - - def __init__(self, *, pool: AsyncConnectionPool) -> None: - try: - from psycopg_pool import AsyncConnectionPool as Pool - except ImportError: - pass - else: - if not isinstance(pool, Pool): - raise InlineStorageError("INVALID_INPUT") - self._pool = pool - return - raise ImportError("PostgreSQL inline storage requires adcp[pg]") - - def __repr__(self) -> str: - return f"{type(self).__name__}()" - - async def _ready_on(self, connection: Any) -> None: - installed = await schema_objects(connection) - if not REQUIRED_OBJECTS or any( - installed.get(key, {}).get(field) != value[field] - for key, value in REQUIRED_OBJECTS.items() - for field in ("fingerprint", "enabled") - ): - raise InlineStorageError("SCHEMA_UNREADY") - - @asynccontextmanager - async def _transaction(self) -> AsyncIterator[Any]: - from psycopg.errors import UndefinedTable - - async with self._pool.connection() as connection, connection.transaction(): - # DO NOTHING's conflict winner must be visible to the next SELECT, - # regardless of a pool's default isolation level. - await connection.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED") - try: - await connection.execute( - "LOCK TABLE reporting_inline_objects, reporting_inline_seals " - "IN ACCESS SHARE MODE" - ) - except UndefinedTable: - raise InlineStorageError("SCHEMA_UNREADY") from None - await self._ready_on(connection) - yield connection - - @_redact - async def create_schema(self) -> None: - """Install the standalone additive migration and verify it atomically.""" - async with self._pool.connection() as connection, connection.transaction(): - await connection.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED") - await connection.execute( - files("adcp.reporting.ledger").joinpath("reporting_inline_storage.sql").read_text() - ) - await self._ready_on(connection) - - @_redact - async def check_ready(self) -> None: - """Verify required schema objects, including constraints and write guards.""" - async with self._pool.connection() as connection, connection.transaction(): - await self._ready_on(connection) - - -class PgReportingStagingStore(_PgStorage): - """Immutable account-qualified content storage using a borrowed async pool.""" - - def __init__( - self, *, pool: AsyncConnectionPool, max_payload_bytes: int = INLINE_STAGING_MAX_BYTES - ) -> None: - if ( - type(max_payload_bytes) is not int - or not 1 <= max_payload_bytes <= INLINE_STAGING_MAX_BYTES - ): - raise InlineStorageError("INVALID_INPUT") - super().__init__(pool=pool) - self._max_payload_bytes = max_payload_bytes - - async def _read_on(self, connection: Any, account_id: str, digest: str) -> bytes: - row = await ( - await connection.execute( - "SELECT CASE WHEN octet_length(payload) <= %s THEN payload END" - " FROM reporting_inline_objects WHERE account_id = %s AND payload_sha256 = %s", - (self._max_payload_bytes, account_id, digest), - ) - ).fetchone() - if row is None: - raise InlineStorageError("NOT_FOUND") - payload = row[0] - if type(payload) is not bytes or await _payload_digest(payload) != digest: - raise InlineStorageError("INTEGRITY_FAILED") - return payload - - async def _prepare_object( - self, *, account_id: str, source_execution_key: str, ordinal: int, payload: bytes - ) -> PreparedBackendObjectV1: - _object_inputs(account_id, source_execution_key, ordinal, payload, self._max_payload_bytes) - digest = await _payload_digest(payload) - return PreparedBackendObjectV1(account_id, source_execution_key, ordinal, payload, digest) - - async def _stage_on( - self, connection: Any, prepared: PreparedBackendObjectV1 - ) -> tuple[str, str]: - """Stage on the caller's audited transaction; the returned identity is tentative. - - Caller owns account-lock ordering and the full transaction/error boundary. - No checkout, task, transaction, commit, drain or fence is owned here. - """ - if type(prepared) is not PreparedBackendObjectV1: - raise InlineStorageError("INVALID_INPUT") - prepared._validate(self._max_payload_bytes) - await connection.execute( - "INSERT INTO reporting_inline_objects (account_id, payload_sha256, payload)" - " VALUES (%s, %s, %s) ON CONFLICT (account_id, payload_sha256) DO NOTHING", - (prepared.account_id, prepared.payload_sha256, prepared.payload), - ) - if ( - await self._read_on(connection, prepared.account_id, prepared.payload_sha256) - != prepared.payload - ): - raise InlineStorageError("INTEGRITY_FAILED") - return _reference(prepared.account_id, prepared.payload_sha256), prepared.payload_sha256 - - @_redact - async def stage( - self, *, account_id: str, source_execution_key: str, ordinal: int, payload: bytes - ) -> tuple[str, str]: - prepared = await self._prepare_object( - account_id=account_id, - source_execution_key=source_execution_key, - ordinal=ordinal, - payload=payload, - ) - async with self._transaction() as connection: - return await self._stage_on(connection, prepared) - - @_redact - async def read( - self, - *, - object_ref: str, - object_generation: str, - account_id: str, - source_scope: Mapping[str, Any], - cancel: asyncio.Event, - ) -> bytes: - _account(account_id) - if type(object_generation) is not str or _DIGEST.fullmatch(object_generation) is None: - raise InlineStorageError("INVALID_INPUT") - if type(object_ref) is not str or object_ref != _reference(account_id, object_generation): - raise InlineStorageError("NOT_FOUND") - if cancel.is_set(): - raise asyncio.CancelledError - async with self._transaction() as connection: - payload = await self._read_on(connection, account_id, object_generation) - if cancel.is_set(): - raise asyncio.CancelledError - return payload - - -class PgReportingSealStore(_PgStorage): - """First committed seal wins for each trusted account and execution key.""" - - async def _get_on(self, connection: Any, account_id: str, key: str) -> SealedSlice | None: - row = await ( - await connection.execute( - "SELECT staged_commit_ref, manifest_sha256, byte_count," - " CASE WHEN octet_length(manifest) <= %s THEN manifest END" - " FROM reporting_inline_seals WHERE account_id = %s AND source_execution_key = %s", - (SOURCE_BATCH_MANIFEST_MAX_BYTES_V1, account_id, key), - ) - ).fetchone() - if row is None: - return None - reference: SourceBatchManifestReferenceV1 | None - try: - reference = SourceBatchManifestReferenceV1( - staged_commit_ref=row[0], manifest_sha256=row[1], byte_count=row[2] - ) - except Exception: - reference = None - if reference is None: - raise InlineStorageError("INTEGRITY_FAILED") - return _seal( - account_id, - key, - SealedSlice(reference=reference, manifest_bytes=row[3]), - "INTEGRITY_FAILED", - ) - - @_redact - async def get(self, *, account_id: str, source_execution_key: str) -> SealedSlice | None: - _identity(account_id, source_execution_key) - async with self._transaction() as connection: - return await self._get_on(connection, account_id, source_execution_key) - - def _prepare_seal( - self, *, account_id: str, source_execution_key: str, sealed: SealedSlice - ) -> PreparedBackendSealV1: - _prepared_identity(account_id, source_execution_key) - candidate = _seal(account_id, source_execution_key, sealed, "INVALID_INPUT") - reference = candidate.reference - return PreparedBackendSealV1( - account_id, - source_execution_key, - reference.staged_commit_ref, - reference.manifest_sha256, - reference.byte_count, - candidate.manifest_bytes, - ) - - async def _put_on(self, connection: Any, prepared: PreparedBackendSealV1) -> SealedSlice: - """Return the verified stored winner on the caller's audited transaction. - - The neutral value proves no admission, referenced-object existence or - commit. Caller owns account locks, cancellation, settlement and any fence. - """ - if type(prepared) is not PreparedBackendSealV1: - raise InlineStorageError("INVALID_INPUT") - prepared._validate() - # Build a fresh reference, then revalidate it inside the existing closed - # candidate validator. No mutable caller model is retained or trusted. - reference = SourceBatchManifestReferenceV1.model_construct( - staged_commit_ref=prepared.staged_commit_ref, - manifest_sha256=prepared.manifest_sha256, - byte_count=prepared.byte_count, - ) - candidate = _seal( - prepared.account_id, - prepared.source_execution_key, - SealedSlice(reference=reference, manifest_bytes=prepared.manifest_bytes), - "INVALID_INPUT", - ) - await connection.execute( - "INSERT INTO reporting_inline_seals (account_id, source_execution_key," - " staged_commit_ref, manifest_sha256, byte_count, manifest)" - " VALUES (%s, %s, %s, %s, %s, %s)" - " ON CONFLICT (account_id, source_execution_key) DO NOTHING", - ( - prepared.account_id, - prepared.source_execution_key, - candidate.reference.staged_commit_ref, - candidate.reference.manifest_sha256, - candidate.reference.byte_count, - candidate.manifest_bytes, - ), - ) - winner = await self._get_on(connection, prepared.account_id, prepared.source_execution_key) - if winner is None: - raise InlineStorageError("INTEGRITY_FAILED") - return winner - - @_redact - async def put( - self, *, account_id: str, source_execution_key: str, sealed: SealedSlice - ) -> SealedSlice: - prepared = self._prepare_seal( - account_id=account_id, source_execution_key=source_execution_key, sealed=sealed - ) - async with self._transaction() as connection: - return await self._put_on(connection, prepared) diff --git a/src/adcp/reporting/ledger/__init__.py b/src/adcp/reporting/ledger/__init__.py index 9ec33ee75..52c46d7ef 100644 --- a/src/adcp/reporting/ledger/__init__.py +++ b/src/adcp/reporting/ledger/__init__.py @@ -21,9 +21,10 @@ from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( - PgReportingLedgerStore, ProducerOfferings, ReportingProducer, - ReportingStatusCaller, ReportingStatusHandler, + ProducerOfferings, ReportingProducer, ReportingStatusCaller, ReportingStatusHandler, ) + # pip install "adcp-reporting-storage[pg]" + from adcp_reporting_storage import PgReportingLedgerStore store = PgReportingLedgerStore(pool=pool) await store.create_schema() @@ -43,7 +44,9 @@ No Temporal, no Celery, no Redis lock. Durability lives in the store; the worker is a stateless leased turn, and the status handler is a pure projection -you can mount from :mod:`adcp.server` or any framework. +you can mount from :mod:`adcp.server` or any framework. This package holds +the store *protocols* and the in-memory reference store; the PostgreSQL store +and the row storage providers ship separately as ``adcp-reporting-storage``. Health is derived, never stored -- a pure function of obligations, revisions, and the snapshot clock, so it cannot go stale and two readers of one snapshot @@ -67,12 +70,6 @@ validate_currency, ) from adcp.reporting.evidence import ReportingCanonicalDigest -from adcp.reporting.ledger.change_feed import ( - ReportingChangeCursorExpiredError, - ReportingChangeKindV1, - ReportingChangeRecordV1, - ReportingChangesPageV1, -) from adcp.reporting.ledger.consumer_status import ( ConsumerMismatch, ConsumerStatusDisabledError, @@ -160,7 +157,6 @@ derive_period, iso_duration_to_timedelta, ) -from adcp.reporting.ledger.period_retirement import ReportingPeriodRetirementV1 from adcp.reporting.ledger.producer import ( CurrencyResolver, FixedCurrencyResolver, @@ -171,12 +167,7 @@ revision_binding_algorithm, revision_content_sha256, ) -from adcp.reporting.ledger.row_retention import ReportingRowIntentSweepV1, ReportingRowPruneV1 -from adcp.reporting.ledger.row_storage import ( - ReportingObjectRowBindingV1, - ReportingPostgresRowBindingV1, - ReportingRowBindingSelectionInputV1, - ReportingRowStorageOptionsV1, +from adcp.reporting.ledger.row_storage_errors import ( ReportingRowStoreError, ReportingRowStoreErrorCode, ) @@ -200,6 +191,9 @@ LedgerPage, ReportingLedgerStore, ReportingRowPage, + ReportingRowStagingStore, + ReportingSourcePublicationStore, + ReportingWorkerRetryPolicy, RestatementCheckpoint, RestatementCheckpointStore, RetryScheduleEntry, @@ -218,8 +212,6 @@ ) if TYPE_CHECKING: - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore from adcp.reporting.ledger.status_server import ( ReportingStatusCallerResolver, ReportingStatusNotificationHandler, @@ -256,16 +248,10 @@ "LedgerPage", "LedgerSnapshot", "ObligationProjection", - "PgReportingLedgerStore", - "PgReportingReconciliationStore", "ProducerOfferings", "ReportingAdjustmentReceiptRecord", "ReportingAdjustmentRecord", "ReportingCanonicalDigest", - "ReportingChangeCursorExpiredError", - "ReportingChangeKindV1", - "ReportingChangeRecordV1", - "ReportingChangesPageV1", "ReportingConfiguration", "ReportingConfigurationGenerationKey", "ReportingControlTotalRecord", @@ -294,10 +280,7 @@ "ReportingObligationDeliveryRecord", "ReportingObligationRecord", "ReportingPeriodBoundary", - "ReportingObjectRowBindingV1", - "ReportingPeriodRetirementV1", "ReportingPhysicalChecksum", - "ReportingPostgresRowBindingV1", "ReportingProducer", "ReportingProductionStatus", "ReportingReceiptKey", @@ -317,11 +300,10 @@ "ReportingResourceRecord", "ReportingRevisionReceiptRecord", "ReportingRevisionRecord", - "ReportingRowBindingSelectionInputV1", - "ReportingRowIntentSweepV1", "ReportingRowPage", - "ReportingRowPruneV1", - "ReportingRowStorageOptionsV1", + "ReportingRowStagingStore", + "ReportingSourcePublicationStore", + "ReportingWorkerRetryPolicy", "ReportingRowStoreError", "ReportingRowStoreErrorCode", "RestatementCheckpoint", @@ -362,15 +344,7 @@ def __getattr__(name: str) -> object: - """Keep core evidence imports independent of database and server adapters.""" - if name == "PgReportingLedgerStore": - from adcp.reporting.ledger.pg import PgReportingLedgerStore - - return PgReportingLedgerStore - if name == "PgReportingReconciliationStore": - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - - return PgReportingReconciliationStore + """Keep core evidence imports independent of the server adapters.""" if name in {"ReportingStatusCallerResolver", "ReportingStatusNotificationHandler"}: from adcp.reporting.ledger import status_server diff --git a/src/adcp/reporting/ledger/_record_rows.py b/src/adcp/reporting/ledger/_record_rows.py new file mode 100644 index 000000000..50d08fe44 --- /dev/null +++ b/src/adcp/reporting/ledger/_record_rows.py @@ -0,0 +1,314 @@ +"""Row codecs for the shared PostgreSQL record layout. + +These functions translate between the SQL column tuples of the ledger tables and +the record dataclasses, and between records and their canonical JSON payloads. +They touch no database: the PostgreSQL stores (``adcp-reporting-storage``) use +them to read rows, and :func:`adcp.reporting.ledger.status_snapshot.snapshot_from_storage` +uses them to decode a stored status snapshot without a driver. + +The column order is the storage contract the JavaScript SDK shares; change a +column list only together with the DDL in ``adcp-reporting-storage``. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from datetime import datetime, timedelta +from typing import Any + +from adcp.reporting.evidence import ReportingCanonicalDigest, ReportingControlTotalRecord +from adcp.reporting.ledger.models import ( + ConsumerStatusRecord, + ReportingAdjustmentRecord, + ReportingConfiguration, + ReportingDefinitionBinding, + ReportingIssueLifecycle, + ReportingObligationRecord, + ReportingPeriodBoundary, + ReportingRevisionRecord, + ReportingScheduleSpec, +) +from adcp.reporting.ledger.store import _utc, managed_revision_metadata + +_OBLIGATION_COLUMNS = ( + "reporting_obligation_id, account_id, delivery_config_id, delivery_config_version," + " report_definition_id, reporting_profile, feed_purpose, period_key, period_start," + " period_end, source_timezone, expected_at, scope_resolved_at," + " automated_recovery_deadline_at, required_finality, coverage_status, media_buy_ids," + " package_ids, schedule, definition, created_at, currency, consumer_id" +) + + +_REVISION_COLUMNS = ( + "reporting_revision_id, account_id, reporting_obligation_id, finality," + " revision_content_sha256, row_count, control_totals, observed_at, data_through," + " created_at, supersedes_reporting_revision_id, finality_basis, finality_policy_id," + " finalized_at, readable, readable_at_commit, source_publication_id, source_manifest_sha256," + " canonical_content_digest, managed_control_totals" +) + + +_ADJUSTMENT_COLUMNS = ( + "reporting_adjustment_id, account_id, adjusts_reporting_revision_id, reason_code," + " reason_detail, accounting_period_start, accounting_period_end, control_total_deltas," + " correction_observed_at, created_at, managed_control_total_deltas" +) + + +_STATUS_COLUMNS = ( + "s.reporting_status_id, s.account_id, s.consumer_id, s.delivery_config_id," + " s.delivery_config_version, s.report_definition_id, s.period_start, s.period_end," + " s.period_source_timezone, s.consumer_status, s.status_as_of, s.recorded_at," + " s.supersedes_reporting_status_id, s.reporting_obligation_id, s.reporting_revision_id," + " s.observed_revision_content_sha256, s.failure_code, s.mismatch_code," + " s.consumer_commit_ref, s.seller_ledger_snapshot_id, s.seller_ledger_as_of, s.superseded" +) + + +_STATUS_COLUMNS_BARE = _STATUS_COLUMNS.replace("s.", "") + + +_ISSUE_COLUMNS = ( + "issue_key, account_id, generation, issue_id, consumer_id, opened_at, issue_state," + " external_ref, retired_at" +) + + +def _issue_from_row(row: Sequence[Any]) -> ReportingIssueLifecycle: + return ReportingIssueLifecycle( + issue_key=row[0], + account_id=row[1], + generation=row[2], + issue_id=row[3], + consumer_id=row[4], + opened_at=_utc(row[5]), + issue_state=row[6], + external_ref=row[7], + retired_at=_utc(row[8]) if row[8] else None, + waived_reporting_status_id=row[9] if len(row) > 9 else None, + waived_conflict_sha256=row[10] if len(row) > 10 else None, + ) + + +def _schedule_payload(schedule: ReportingScheduleSpec) -> dict[str, Any]: + return { + "period_duration": schedule.period_duration, + "delivery_sla": schedule.delivery_sla, + "alignment": schedule.alignment, + "period_timezone": schedule.period_timezone, + "period_anchor": ( + _utc(schedule.period_anchor).isoformat() if schedule.period_anchor else None + ), + } + + +def _definition_payload( + definition: ReportingDefinitionBinding | None, +) -> dict[str, Any] | None: + return definition.to_storage() if definition is not None else None + + +def _definition_from_payload(payload: dict[str, Any] | None) -> ReportingDefinitionBinding | None: + return ReportingDefinitionBinding(**payload) if payload else None + + +def _schedule_from_payload(payload: dict[str, Any]) -> ReportingScheduleSpec: + anchor = payload.get("period_anchor") + return ReportingScheduleSpec( + period_duration=payload["period_duration"], + delivery_sla=payload["delivery_sla"], + alignment=payload.get("alignment", "utc"), + period_timezone=payload.get("period_timezone"), + period_anchor=datetime.fromisoformat(anchor) if anchor else None, + ) + + +def _configuration_payload(configuration: ReportingConfiguration) -> dict[str, Any]: + return { + "account_id": configuration.account_id, + "report_definition_id": configuration.report_definition_id, + "reporting_profile": configuration.reporting_profile, + "feed_purpose": configuration.feed_purpose, + "required_finality": configuration.required_finality, + "account_timezone": configuration.account_timezone, + "media_buy_ids": sorted(configuration.media_buy_ids), + "schedule": _schedule_payload(configuration.schedule), + "definition": _definition_payload(configuration.definition), + } + + +def _configuration_from_row(row: Sequence[Any]) -> ReportingConfiguration: + return ReportingConfiguration( + delivery_config_id=row[0], + delivery_config_version=row[1], + account_id=row[2], + report_definition_id=row[3], + reporting_profile=row[4], + feed_purpose=row[5], + required_finality=row[6], + account_timezone=row[7], + schedule=_schedule_from_payload(row[8]), + media_buy_ids=tuple(row[9] or ()), + activated_at=_utc(row[10]) if row[10] else None, + deactivated_at=_utc(row[11]) if row[11] else None, + automated_recovery_window=timedelta(seconds=float(row[12])), + status_retention_days=row[13], + definition=_definition_from_payload(row[14]), + authoritative_party=row[15], + consumer_id=row[16], + quarantined=row[17], + ) + + +def _obligation_from_row(row: Sequence[Any]) -> ReportingObligationRecord: + return ReportingObligationRecord( + reporting_obligation_id=row[0], + account_id=row[1], + delivery_config_id=row[2], + delivery_config_version=row[3], + report_definition_id=row[4], + reporting_profile=row[5], + feed_purpose=row[6], + period=ReportingPeriodBoundary( + period_key=row[7], + start=_utc(row[8]), + end=_utc(row[9]), + source_timezone=row[10], + expected_at=_utc(row[11]), + ), + scope_resolved_at=_utc(row[12]), + automated_recovery_deadline_at=_utc(row[13]), + required_finality=row[14], + coverage_status=row[15], + media_buy_ids=tuple(row[16] or ()), + package_ids=tuple(row[17] or ()), + schedule=_schedule_from_payload(row[18]), + definition=_definition_from_payload(row[19]), + created_at=_utc(row[20]), + currency=row[21], + consumer_id=row[22], + ) + + +def _revision_from_row(row: Sequence[Any]) -> ReportingRevisionRecord: + return ReportingRevisionRecord( + reporting_revision_id=row[0], + account_id=row[1], + reporting_obligation_id=row[2], + finality=row[3], + revision_content_sha256=row[4], + row_count=int(row[5]), + control_totals=tuple((name, value) for name, value in (row[6] or [])), + observed_at=_utc(row[7]), + data_through=_utc(row[8]) if row[8] else None, + created_at=_utc(row[9]), + supersedes_reporting_revision_id=row[10], + finality_basis=row[11], + finality_policy_id=row[12], + finalized_at=_utc(row[13]) if row[13] else None, + readable=row[14], + readable_at_commit=row[15], + source_publication_id=row[16], + source_manifest_sha256=row[17], + canonical_content_digest=( + ReportingCanonicalDigest.from_wire(row[18]) if row[18] is not None else None + ), + managed_control_totals=( + tuple(ReportingControlTotalRecord.from_wire(item) for item in row[19]) + if row[19] is not None + else None + ), + ) + + +def _adjustment_from_row(row: Sequence[Any]) -> ReportingAdjustmentRecord: + return ReportingAdjustmentRecord( + reporting_adjustment_id=row[0], + account_id=row[1], + adjusts_reporting_revision_id=row[2], + reason_code=row[3], + reason_detail=row[4], + accounting_period_start=_utc(row[5]), + accounting_period_end=_utc(row[6]), + control_total_deltas=tuple((name, value) for name, value in (row[7] or [])), + correction_observed_at=_utc(row[8]), + created_at=_utc(row[9]), + managed_control_total_deltas=( + tuple(ReportingControlTotalRecord.from_wire(item) for item in row[10]) + if row[10] is not None + else None + ), + ) + + +def _status_from_row(row: Sequence[Any]) -> ConsumerStatusRecord: + return ConsumerStatusRecord( + reporting_status_id=row[0], + account_id=row[1], + consumer_id=row[2], + delivery_config_id=row[3], + delivery_config_version=row[4], + report_definition_id=row[5], + period_start=_utc(row[6]), + period_end=_utc(row[7]), + period_source_timezone=row[8], + consumer_status=row[9], + status_as_of=_utc(row[10]), + recorded_at=_utc(row[11]), + supersedes_reporting_status_id=row[12], + reporting_obligation_id=row[13], + reporting_revision_id=row[14], + observed_revision_content_sha256=row[15], + failure_code=row[16], + mismatch_code=row[17], + consumer_commit_ref=row[18], + seller_ledger_snapshot_id=row[19], + seller_ledger_as_of=_utc(row[20]) if row[20] else None, + superseded=row[21], + ) + + +def _consumer_status_payload(status: ConsumerStatusRecord) -> dict[str, Any]: + payload: dict[str, Any] = { + "chain": list(status.chain_key), + "consumer_status": status.consumer_status, + "status_as_of": _utc(status.status_as_of).isoformat(), + "supersedes": status.supersedes_reporting_status_id, + "obligation": status.reporting_obligation_id, + "revision": status.reporting_revision_id, + "digest": status.observed_revision_content_sha256, + "failure_code": status.failure_code, + } + # Conditional on purpose. ``canonical_json_utf8_v1`` encodes ``None`` as + # ``null``, so including the key unconditionally would change the digest of + # every statement that has no mismatch_code -- i.e. every row an rc.2 SDK + # wrote. After an in-place upgrade a buyer's exact retry would then fail + # with STATUS_IDENTITY_CONFLICT instead of replaying as ``unchanged``, + # which is the one thing an idempotent append-only surface must never do. + # Omitting the key when absent keeps rc.2 digests byte-identical while a + # code-only change is still a conflict: "the metric is missing" and "the + # currency is wrong" are different claims and must not share one immutable + # identity. + if status.mismatch_code is not None: + payload["mismatch_code"] = status.mismatch_code + return payload + + +def _revision_payload(revision: ReportingRevisionRecord) -> dict[str, Any]: + payload = { + "finality": revision.finality, + "revision_content_sha256": revision.revision_content_sha256, + "row_count": revision.row_count, + "control_totals": [list(item) for item in revision.control_totals], + "obligation": revision.reporting_obligation_id, + "supersedes": revision.supersedes_reporting_revision_id, + } + if revision.canonical_content_digest is not None: + payload["canonical_content_digest"] = revision.canonical_content_digest.to_wire() + if revision.managed_control_totals is not None: + payload["managed_control_totals"] = [ + item.to_wire() for item in revision.managed_control_totals + ] + if revision.canonical_content_digest is not None or revision.managed_control_totals is not None: + payload["managed_metadata"] = managed_revision_metadata(revision) + return payload diff --git a/src/adcp/reporting/ledger/producer.py b/src/adcp/reporting/ledger/producer.py index 807769ef8..449acde58 100644 --- a/src/adcp/reporting/ledger/producer.py +++ b/src/adcp/reporting/ledger/producer.py @@ -75,6 +75,9 @@ LeasedConfiguration, LedgerConflictError, ReportingLedgerStore, + ReportingRowStagingStore, + ReportingSourcePublicationStore, + ReportingWorkerRetryPolicy, RestatementCheckpointStore, RetryScheduleEntry, RetryScheduleStore, @@ -454,11 +457,9 @@ async def run_worker(self) -> WorkerTurn: deadlocks and lock timeouts at most twice, after rollback and fenced release. Each retry must acquire a new lease; other failures propagate. """ - from adcp.reporting.ledger.pg import PgReportingLedgerStore - retryable = ( - self._store._worker_lock_errors() - if isinstance(self._store, PgReportingLedgerStore) + self._store.worker_lock_errors() + if isinstance(self._store, ReportingWorkerRetryPolicy) else () ) for attempt in range(3): @@ -1542,10 +1543,11 @@ async def _source_publication( if not isinstance(self._source, ReportingProductionSource): yield None return - publication = getattr(self._store, "_source_publication", None) - if publication is None: + if not isinstance(self._store, ReportingSourcePublicationStore): raise TypeError("production source publication requires an SDK account lock") - async with publication(configuration.account_id, seals=seals) as publish_seal: + async with self._store.source_publication( + configuration.account_id, seals=seals + ) as publish_seal: self._check_source_authorization(configuration, offering_id, admitted_constituents) yield publish_seal @@ -1863,15 +1865,15 @@ async def commit_revision_from_manifest( revision = verified_publication(self._revision_verifier, obligation, revision, rows) # An object-bound revision uploads its chunks before the ledger transaction, # never inside it, so a slow store cannot hold the account lock. - stage = getattr(self._store, "stage_revision_rows", None) - staged = bool(await stage(revision, rows)) if stage is not None else False + staging = self._store if isinstance(self._store, ReportingRowStagingStore) else None + staged = staging is not None and bool(await staging.stage_revision_rows(revision, rows)) try: committed = await self._commit_publication( obligation, manifest, revision, rows, finality, now, acquisition ) finally: - if staged: - self._store.discard_staged_revision_rows(revision) # type: ignore[attr-defined] + if staged and staging is not None: + staging.discard_staged_revision_rows(revision) turn.revisions_committed.append(committed.reporting_revision_id) return committed diff --git a/src/adcp/reporting/ledger/status_snapshot.py b/src/adcp/reporting/ledger/status_snapshot.py index e58be2cf4..3c9b02226 100644 --- a/src/adcp/reporting/ledger/status_snapshot.py +++ b/src/adcp/reporting/ledger/status_snapshot.py @@ -4,7 +4,6 @@ from collections.abc import Callable from dataclasses import replace -from datetime import datetime from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable from adcp.reporting._timestamp import aware_timestamp @@ -13,7 +12,6 @@ from adcp.reporting.ledger.status_projection import ( ReportingStatusSnapshot, StatusLifecycleIntent, - apply_intents_to_snapshot, lifecycle_intents, status_matches_obligation, ) @@ -21,7 +19,6 @@ if TYPE_CHECKING: from adcp.reporting.ledger.models import ReportingCaller - from adcp.reporting.ledger.pg import PgReportingLedgerStore from adcp.reporting.ledger.store import InMemoryReportingLedgerStore @@ -173,53 +170,9 @@ def settle_memory_snapshot( raise ReportingNotificationError("status_lifecycle_did_not_converge") -_TABLES = { - "configurations": "reporting_configurations", - "obligations": "reporting_obligations", - "revisions": "reporting_revisions", - "statuses": "reporting_consumer_statuses", - "lifecycles": "reporting_issue_lifecycle", - "waivers": "reporting_issue_waiver_bindings", - "issue_scopes": "reporting_issue_status_scopes", - "adjustments": "reporting_adjustments", - "changes": "reporting_ledger_changes", - "consumers": "reporting_reconciliation_heads", -} - - -async def read_snapshot_on( - connection: Any, - *, - account_id: str, - clock: Callable[[], datetime] | None = None, - as_of: datetime | None = None, - include_issue_scopes: bool = True, -) -> ReportingStatusSnapshot: - from adcp.reporting.ledger.pg import _waiver_storage_on - from adcp.reporting.outbox.pg import database_now - - at = as_of or await database_now(connection, clock) - raw: dict[str, Any] = {"account_id": account_id, "as_of": at.isoformat()} - for key, table in _TABLES.items(): - if key == "issue_scopes" and not include_issue_scopes: - continue - if key == "waivers" and not await _waiver_storage_on(connection): - continue - # Table names are the closed SDK constants above. Parameters are bound. - rows = await ( - await connection.execute( - f"SELECT to_jsonb(r) FROM {table} r WHERE account_id = %s", # nosec B608 - (account_id,), - ) - ).fetchall() - raw[key] = [row[0] for row in rows] - return snapshot_from_storage(raw) - - def snapshot_from_storage(raw: dict[str, Any]) -> ReportingStatusSnapshot: """Decode the exact SQL boundary input through the ledger's existing mappers.""" - from adcp.reporting.ledger.notification_models import decode_status_scope - from adcp.reporting.ledger.pg import ( + from adcp.reporting.ledger._record_rows import ( _ADJUSTMENT_COLUMNS, _ISSUE_COLUMNS, _OBLIGATION_COLUMNS, @@ -232,6 +185,7 @@ def snapshot_from_storage(raw: dict[str, Any]) -> ReportingStatusSnapshot: _revision_from_row, _status_from_row, ) + from adcp.reporting.ledger.notification_models import decode_status_scope dates = { "activated_at", @@ -319,135 +273,3 @@ def decode(key: str, columns: str, builder: Callable[[Any], Any]) -> tuple[Any, for row in raw.get("changes", []) ), ) - - -async def apply_intents_on( - store: PgReportingLedgerStore, connection: Any, intents: tuple[StatusLifecycleIntent, ...] -) -> None: - """No pooled calls. The source/projector owns account and checkpoint locks.""" - for intent in intents: - issue = intent.lifecycle - if intent.action == "ensure_mismatch": - await store._ensure_issue_opened_on( - connection, - issue_key=issue.issue_key, - account_id=issue.account_id, - consumer_id=issue.consumer_id, - observed_at=issue.opened_at, - status_scope=intent.scope, - enqueue=False, - ) - elif intent.action == "retire_mismatch": - assert issue.retired_at is not None - await store._retire_issue_on( - connection, - issue_key=issue.issue_key, - account_id=issue.account_id, - at=issue.retired_at, - status_scope=intent.scope, - enqueue=False, - ) - else: - await store._dirty_issue(connection, issue, intent.scope, enqueue=False) - - -async def persist_replay_lifecycles_on( - store: PgReportingLedgerStore, connection: Any, snapshot: ReportingStatusSnapshot -) -> None: - """Persist derived historical occurrences without regressing later evidence. - - Captured source rows remain immutable. These are the same public lifecycle - rows used by polling, including repairs for an old writer's interrupted - status/issue pair. The enclosing account lock fences concurrent ingestion. - """ - from adcp.reporting.ledger.pg import _save_waiver_binding - - scopes = dict(snapshot.issue_scopes) - for issue in sorted(snapshot.lifecycles, key=lambda i: (i.issue_key, i.generation)): - await connection.execute( - "INSERT INTO reporting_issue_lifecycle" - " (account_id, issue_key, generation, issue_id, consumer_id, opened_at," - " issue_state, external_ref, retired_at) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)" - " ON CONFLICT (account_id, issue_key, generation) DO NOTHING", - ( - issue.account_id, - issue.issue_key, - issue.generation, - issue.issue_id, - issue.consumer_id, - issue.opened_at, - issue.issue_state, - issue.external_ref, - issue.retired_at, - ), - ) - await _save_waiver_binding(connection, issue) - if issue.issue_state != "open": - await connection.execute( - "UPDATE reporting_issue_lifecycle SET issue_state=%s, external_ref=%s," - " retired_at=%s" - " WHERE account_id=%s AND issue_key=%s AND generation=%s" - " AND issue_state IN ('open','acknowledged')", - ( - issue.issue_state, - issue.external_ref, - issue.retired_at, - issue.account_id, - issue.issue_key, - issue.generation, - ), - ) - scope = scopes.get(issue.issue_id) - if scope is not None: - # Existing scope may already be more precise because a later source - # transaction attached this occurrence. Historical replay retains it. - row = await ( - await connection.execute( - "SELECT scope FROM reporting_issue_status_scopes" - " WHERE account_id=%s AND issue_id=%s", - (issue.account_id, issue.issue_id), - ) - ).fetchone() - if row is not None: - from adcp.reporting.ledger.notification_models import ( - decode_status_scope, - validate_scope_refinement, - ) - - old = decode_status_scope(row[0]) - try: - validate_scope_refinement(old, scope) - except ReportingNotificationError: - validate_scope_refinement(scope, old) - scope = old - await store._dirty_issue(connection, issue, scope, enqueue=False) - - -async def settle_snapshot_on( - store: PgReportingLedgerStore, - connection: Any, - *, - account_id: str, - as_of: datetime | None = None, -) -> ReportingStatusSnapshot: - from adcp.reporting.outbox.pg import database_now - - at = as_of or await database_now(connection, store._clock) - has_scope_storage = await store._issue_scope_storage_on(connection) - for _ in range(3): - snapshot = await read_snapshot_on( - connection, account_id=account_id, as_of=at, include_issue_scopes=has_scope_storage - ) - if not has_scope_storage: - # Pre-outbox Core schemas retain lifecycle but have no scope table. - # Derive scope from authenticated status evidence for this read; - # notification-enabled participants require durable scope storage. - snapshot = apply_intents_to_snapshot( - snapshot, - tuple(i for i in lifecycle_intents(snapshot) if i.action == "refine_scope"), - ) - intents = lifecycle_intents(snapshot) - if not intents: - return snapshot - await apply_intents_on(store, connection, intents) - raise ReportingNotificationError("status_lifecycle_did_not_converge") diff --git a/src/adcp/reporting/ledger/store.py b/src/adcp/reporting/ledger/store.py index a5e4cb32d..b91762212 100644 --- a/src/adcp/reporting/ledger/store.py +++ b/src/adcp/reporting/ledger/store.py @@ -26,7 +26,7 @@ conformance target. It is genuinely usable in tests and single-process pilots, and it is not durable -- Reliable Reporting's whole premise is retained evidence, so production wants -:class:`~adcp.reporting.ledger.pg.PgReportingLedgerStore` or your own. +:class:`~adcp_reporting_storage.ledger.pg.PgReportingLedgerStore` or your own. """ from __future__ import annotations @@ -36,7 +36,7 @@ import hashlib import json from collections.abc import AsyncIterator, Callable, Sequence -from contextlib import asynccontextmanager +from contextlib import AbstractAsyncContextManager, asynccontextmanager from contextvars import ContextVar from copy import deepcopy from dataclasses import dataclass, field, replace @@ -319,6 +319,38 @@ def revision_row_offset(cursor: str | None, revision_id: str, limit: int) -> int return offset +@runtime_checkable +class ReportingRowStagingStore(Protocol): + """Optional: a store that uploads a revision's rows before the ledger transaction. + + The producer stages rows when the store offers it and discards the staged + handle when the commit does not happen. A store that keeps rows inline needs + neither method. See adcp-client-python#1489 for the handle-passing variant. + """ + + async def stage_revision_rows( + self, revision: ReportingRevisionRecord, rows: Sequence[dict[str, Any]] + ) -> object | None: ... + + def discard_staged_revision_rows(self, revision: ReportingRevisionRecord) -> None: ... + + +@runtime_checkable +class ReportingSourcePublicationStore(Protocol): + """Optional: a store that publishes a production source under its account lock.""" + + def source_publication( + self, account_id: str, *, seals: ReportingSealStore | None = None + ) -> AbstractAsyncContextManager[Any]: ... + + +@runtime_checkable +class ReportingWorkerRetryPolicy(Protocol): + """Optional: lock errors a standalone worker turn retries after a fenced release.""" + + def worker_lock_errors(self) -> tuple[type[Exception], ...]: ... + + @runtime_checkable class ReportingLedgerStore(Protocol): """Durable home for obligations, revisions, adjustments, and statuses.""" @@ -841,8 +873,13 @@ async def transaction(self) -> AsyncIterator[InMemoryReportingLedgerStore]: async with self._mutation(): yield self + @property + def clock(self) -> Callable[[], datetime]: + """The instant source for change timestamps and snapshot boundaries.""" + return self._clock + @asynccontextmanager - async def _source_publication( + async def source_publication( self, account_id: str, *, seals: ReportingSealStore | None = None ) -> AsyncIterator[None]: # The memory mutation lock serializes all accounts, including seals. diff --git a/src/adcp/reporting/materializer/__init__.py b/src/adcp/reporting/materializer/__init__.py index 28c7edf26..09934d081 100644 --- a/src/adcp/reporting/materializer/__init__.py +++ b/src/adcp/reporting/materializer/__init__.py @@ -5,10 +5,9 @@ writer is exclusively for tests/development. The durable service owns fencing, retry allocation, target reselection and atomic finish. Production tier and notification activation additionally require the complete seller projection. +The PostgreSQL materializer store is ``adcp_reporting_storage.materializer.pg``. """ -from typing import TYPE_CHECKING - from adcp.reporting.ledger.delivery_models import ( ReportingDeliveryPrincipal, ReportingDestinationBinding, @@ -65,13 +64,9 @@ ReportingMaterializerTurn, ) -if TYPE_CHECKING: - from adcp.reporting.materializer.pg import PgReportingMaterializerStore - __all__ = [ "InMemoryReportingMaterializerStore", "MaterializerReason", - "PgReportingMaterializerStore", "ReferenceReportingDestinationWriter", "ReferenceReportingResolver", "ReportingCanonicalization", @@ -114,11 +109,3 @@ "strict_reporting_json", "validate_materialization_target", ] - - -def __getattr__(name: str) -> object: - if name == "PgReportingMaterializerStore": - from adcp.reporting.materializer.pg import PgReportingMaterializerStore - - return PgReportingMaterializerStore - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/adcp/reporting/materializer/capture.py b/src/adcp/reporting/materializer/capture.py index 1898d478e..0ef9aa3b2 100644 --- a/src/adcp/reporting/materializer/capture.py +++ b/src/adcp/reporting/materializer/capture.py @@ -109,26 +109,6 @@ def decode_materializer_boundary(value: dict[str, Any]) -> ReportingMaterializer return result -class _MaterializerQueueConnection: - """Closed table substitution, preserving the finish transaction's connection.""" - - def __init__(self, connection: Any) -> None: - self.connection = connection - - async def execute(self, query: str, params: Any = None) -> Any: - return await self.connection.execute( - query.replace("reporting_notification_", "reporting_materializer_notification_"), params - ) - - -async def enqueue_materializer_event_on(connection: Any, event: ReportingDomainEvent) -> None: - from adcp.reporting.outbox.pg import enqueue_event - - if event.notification_type != "reporting.delivery_ready": - raise ReportingNotificationError("materializer_event_invalid") - await enqueue_event(_MaterializerQueueConnection(connection), event) - - def private_snapshot( snapshot: ReportingStatusSnapshot, caller: ReportingDeliveryPrincipal ) -> ReportingStatusSnapshot: diff --git a/src/adcp/reporting/outbox/__init__.py b/src/adcp/reporting/outbox/__init__.py index f6a0bc090..1987055e6 100644 --- a/src/adcp/reporting/outbox/__init__.py +++ b/src/adcp/reporting/outbox/__init__.py @@ -1,6 +1,7 @@ -"""Optional transactional reporting notifications, activity and status lifecycle.""" +"""Optional transactional reporting notifications, activity and status lifecycle. -from typing import TYPE_CHECKING +The PostgreSQL queues live in ``adcp_reporting_storage.outbox``. +""" from adcp.reporting.ledger.notification_models import ( AdjustmentPublished, @@ -23,6 +24,10 @@ WebhookAttempt, sanitize_activity_url, ) +from adcp.reporting.outbox.durable import ( + ReportingDurableNotificationLedger, + ReportingDurableStatusStore, +) from adcp.reporting.outbox.identity import resolve_reporting_consumer from adcp.reporting.outbox.memory import InMemoryReportingOutbox from adcp.reporting.outbox.models import ( @@ -63,17 +68,13 @@ from adcp.reporting.outbox.support import ReportingActivitySupport from adcp.reporting.outbox.worker import ReportingNotificationWorker -if TYPE_CHECKING: - from adcp.reporting.outbox.pg import PgReportingOutbox - from adcp.reporting.outbox.status_activity_pg import PgReportingActivityUnionStore - from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox, PgStatusNotificationStore - __all__ = [ "ReportingStatusNotificationLifecycle", "ReportingStatusService", "ReportingActivityReader", + "ReportingDurableNotificationLedger", + "ReportingDurableStatusStore", "ReportingStatusSupport", - "PgReportingActivityUnionStore", "StatusChanged", "StatusBoundary", "StatusCheckpoint", @@ -85,8 +86,6 @@ "ReportingStatusSweeper", "InMemoryReportingStatusOutbox", "InMemoryStatusNotificationStore", - "PgReportingStatusOutbox", - "PgStatusNotificationStore", "ActivityOutcome", "ActivityRequest", "ReportingActivityProjector", @@ -102,7 +101,6 @@ "ExpansionLease", "InMemoryReportingOutbox", "MaterializationReady", - "PgReportingOutbox", "ReportingDomainEvent", "ReportingEnvelopeCipher", "ReportingLegacyAuthentication", @@ -120,25 +118,3 @@ "StoredDelivery", "validate_notification_payload", ] - - -def __getattr__(name: str) -> object: - if name == "PgReportingActivityUnionStore": - from adcp.reporting.outbox.status_activity_pg import PgReportingActivityUnionStore - - return PgReportingActivityUnionStore - if name in {"PgReportingStatusOutbox", "PgStatusNotificationStore"}: - from adcp.reporting.outbox.status_pg import ( - PgReportingStatusOutbox, - PgStatusNotificationStore, - ) - - return { - "PgReportingStatusOutbox": PgReportingStatusOutbox, - "PgStatusNotificationStore": PgStatusNotificationStore, - }[name] - if name == "PgReportingOutbox": - from adcp.reporting.outbox.pg import PgReportingOutbox - - return PgReportingOutbox - raise AttributeError(name) diff --git a/src/adcp/reporting/outbox/_capabilities.py b/src/adcp/reporting/outbox/_capabilities.py index 3b36249d3..29ce7b9df 100644 --- a/src/adcp/reporting/outbox/_capabilities.py +++ b/src/adcp/reporting/outbox/_capabilities.py @@ -4,10 +4,14 @@ from typing import TYPE_CHECKING -from adcp.reporting.ledger.delivery import InMemoryReportingReconciliationStore +from adcp.reporting.ledger.delivery import ( + InMemoryReportingReconciliationStore, + ReportingReconciliationStore, +) from adcp.reporting.ledger.delivery_models import ReportingDeliveryScope from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.ledger.store import InMemoryReportingLedgerStore, ReportingLedgerStore +from adcp.reporting.outbox.durable import ReportingDurableNotificationLedger from adcp.reporting.outbox.memory import InMemoryReportingOutbox from adcp.reporting.outbox.routing import ReportingEnvelopeCipher, ReportingNotificationSubscription @@ -39,23 +43,11 @@ async def advertised_notifications( ): raise ReportingNotificationError("notification_chain_unready") else: - # Deliberately lazy: the base SDK and memory implementation need no PG extra. - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore - from adcp.reporting.outbox._schema import validate_schema - from adcp.reporting.outbox.pg import PgReportingOutbox - - if ( - type(worker.outbox) is not PgReportingOutbox - or not isinstance(worker.outbox, PgReportingOutbox) - or type(ledger) not in {PgReportingLedgerStore, PgReportingReconciliationStore} - or not isinstance(ledger, PgReportingLedgerStore) - or ledger._pool is not worker.outbox._pool - or not ledger._notifications_enabled - ): + # A durable ledger proves its own queue, storage and installed schema; + # the base SDK never imports a storage implementation to ask. + if not isinstance(ledger, ReportingDurableNotificationLedger): raise ReportingNotificationError("notification_chain_unready") - async with ledger._pool.connection() as connection, connection.transaction(): - await validate_schema(connection) + await ledger.validate_notification_chain(worker.outbox) # A successful empty resolution is a configured service, not an error. # Every configured mode must also have usable trusted signing material. @@ -97,11 +89,11 @@ async def advertised_notifications( # A capability string cannot turn Core into Managed. These concrete # stores froze and validated the configuration/destination/obligation # relationship transactionally. All references remain immutable. - if not isinstance(ledger, InMemoryReportingReconciliationStore): - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - - if not isinstance(ledger, PgReportingReconciliationStore): - raise ReportingNotificationError("core_delivery_ready_forbidden") + reconciles = isinstance(ledger, InMemoryReportingReconciliationStore) or ( + isinstance(ledger, ReportingDurableNotificationLedger) and ledger.durable_reconciliation + ) + if not reconciles or not isinstance(ledger, ReportingReconciliationStore): + raise ReportingNotificationError("core_delivery_ready_forbidden") binding = await ledger.get_destination_binding( caller=ready_scope.principal, generation_key=ready_scope.generation_key ) diff --git a/src/adcp/reporting/outbox/durable.py b/src/adcp/reporting/outbox/durable.py new file mode 100644 index 000000000..d2903079d --- /dev/null +++ b/src/adcp/reporting/outbox/durable.py @@ -0,0 +1,81 @@ +"""What the notification layer asks of a database-backed ledger. + +``adcp`` ships the in-memory stores and checks their composition inline. Every +other ledger (the PostgreSQL store in ``adcp-reporting-storage``, or an +adopter's own) answers the same questions through these protocols, so the +capability checks in :mod:`adcp.reporting.outbox` never import a storage +implementation. Each ``validate_*`` method raises +:class:`~adcp.reporting.ledger.notification_models.ReportingNotificationError` +with the code the inline checks use, and a store that does not implement a +protocol is treated as not durable. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable + +if TYPE_CHECKING: + from adcp.reporting.outbox.worker import ReportingNotificationWorker + +__all__ = ["ReportingDurableNotificationLedger", "ReportingDurableStatusStore"] + + +@runtime_checkable +class ReportingDurableNotificationLedger(Protocol): + """A ledger whose notification outbox shares its durable storage.""" + + @property + def storage_identity(self) -> object: + """The storage resource every participant must share; compared by identity.""" + ... + + @property + def notifications_enabled(self) -> bool: ... + + @property + def durable_reconciliation(self) -> bool: + """Whether this ledger froze destination bindings transactionally (Managed tier).""" + ... + + async def validate_notification_chain(self, outbox: object) -> None: + """Raise ``notification_chain_unready`` unless ``outbox`` is this ledger's queue. + + Includes the installed-schema check for the notification tables. + """ + ... + + def validate_activity_chain(self, outbox: object) -> None: + """Raise ``activity_chain_unready`` unless ``outbox`` is this ledger's activity queue.""" + ... + + def validate_composite_activity_chain( + self, *, outbox: object, status_store: object, reader: object + ) -> None: + """Raise ``activity_chain_unready`` unless the B+C activity union is closed.""" + ... + + def activity_schema_contract(self, *, composite: bool) -> str: + """A digest of the manifests ``validate_activity_schema`` checks.""" + ... + + async def validate_activity_schema(self, *, composite: bool) -> None: + """Raise ``notification_schema_unready`` unless the activity catalog is installed.""" + ... + + +@runtime_checkable +class ReportingDurableStatusStore(Protocol): + """A status-notification store whose queue shares the ledger's durable storage.""" + + #: The ledger the status queue reads; the activity chain requires the same object. + ledger: Any + #: The store's own status queue, which its worker must drive. + outbox: Any + + async def status_queue_durable(self, worker: ReportingNotificationWorker) -> bool: + """Whether ``worker`` drives this store's own durable queue with its schema installed.""" + ... + + async def managed_delivery_recorded(self, *, account_id: str) -> bool: + """Whether the account holds a frozen destination binding (Managed tier).""" + ... diff --git a/src/adcp/reporting/outbox/memory.py b/src/adcp/reporting/outbox/memory.py index ea5794453..0d49637cf 100644 --- a/src/adcp/reporting/outbox/memory.py +++ b/src/adcp/reporting/outbox/memory.py @@ -181,6 +181,18 @@ async def list_events(self, *, account_id: str) -> tuple[ReportingDomainEvent, . event for (owner, _, _), event in self._state.events.items() if owner == account_id ) + async def next_due_account(self, *, delivery: bool, now: datetime) -> str | None: + """Sample one due queue row; never enumerate adopter accounts.""" + now = aware_utc(now) + async with self._store._lock: + pending = ( + ((key[0], work) for key, (_, work) in self._state.deliveries.items()) + if delivery + else ((key[0], work) for key, work in self._state.expansions.items()) + ) + values = [(work.due_at, account) for account, work in pending if work.available(now)] + return min(values)[1] if values else None + async def claim_expansion( self, *, account_id: str, now: datetime, lease_seconds: float ) -> ExpansionLease | None: diff --git a/src/adcp/reporting/outbox/status_support.py b/src/adcp/reporting/outbox/status_support.py index 33dbb179e..25145b9c2 100644 --- a/src/adcp/reporting/outbox/status_support.py +++ b/src/adcp/reporting/outbox/status_support.py @@ -7,6 +7,7 @@ from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.ledger.status import ReportingStatusHandler +from adcp.reporting.outbox.durable import ReportingDurableStatusStore from adcp.reporting.outbox.routing import ReportingEnvelopeCipher, ReportingNotificationSubscription from adcp.reporting.outbox.status import ( ReportingStatusProjector, @@ -55,47 +56,19 @@ async def queue_durable(self) -> bool: return False if type(self.store) is InMemoryStatusNotificationStore: return False - from adcp.reporting.outbox.status_pg import ( - PgReportingStatusOutbox, - PgStatusNotificationStore, - ) - from adcp.reporting.outbox.status_schema import validate_status_schema - - if ( - type(self.store) is not PgStatusNotificationStore - or not isinstance(self.store, PgStatusNotificationStore) - or type(self.worker.outbox) is not PgReportingStatusOutbox - or self.worker.outbox is not self.store.outbox - or self.store.ledger._pool is not self.store.outbox._pool - or not self.store.ledger._notifications_enabled - or (self.worker.activity is not None and self.worker.activity is not self.worker.outbox) - ): + # A durable store proves its own queue, storage and installed schema. + if not isinstance(self.store, ReportingDurableStatusStore): return False - try: - async with self.store.ledger._pool.connection() as connection: - await validate_status_schema(connection, activity=self.worker.activity is not None) - except ReportingNotificationError: - return False - return True + return await self.store.status_queue_durable(self.worker) async def account_ready(self, *, account_id: str) -> bool: if not await self.queue_durable() or not await self.store.baseline_ready( account_id=account_id ): return False - from adcp.reporting.outbox.status_pg import PgStatusNotificationStore - - assert isinstance(self.store, PgStatusNotificationStore) - async with self.store.ledger._pool.connection() as connection: - managed = await ( - await connection.execute( - "SELECT EXISTS(SELECT 1 FROM reporting_reconciliation_records" - " WHERE account_id=%s AND record_kind='destination_binding')", - (account_id,), - ) - ).fetchone() - if managed is not None and managed[0]: - return False # Managed expiry/reconciliation health ships with D. + assert isinstance(self.store, ReportingDurableStatusStore) + if await self.store.managed_delivery_recorded(account_id=account_id): + return False # Managed expiry/reconciliation health ships with D. subscriptions = await self.worker.subscriptions.list_active( account_id=account_id, notification_type="reporting.status_changed" ) diff --git a/src/adcp/reporting/outbox/support.py b/src/adcp/reporting/outbox/support.py index 031eb669f..ee4a69c9a 100644 --- a/src/adcp/reporting/outbox/support.py +++ b/src/adcp/reporting/outbox/support.py @@ -8,6 +8,10 @@ from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.outbox.activity import ReportingActivityProjector +from adcp.reporting.outbox.durable import ( + ReportingDurableNotificationLedger, + ReportingDurableStatusStore, +) if TYPE_CHECKING: from adcp.reporting.ledger.store import ReportingLedgerStore @@ -15,7 +19,7 @@ from adcp.reporting.outbox.worker import ReportingNotificationWorker -_ProofKey = tuple[str, tuple[int, ...], str, str] +_ProofKey = tuple[str, tuple[int, ...], str] @dataclass @@ -69,9 +73,9 @@ def invalidate_schema_validation(self) -> None: state.positive = None state.flight = None - async def _schema_proven(self, pool: Any, *, composite: bool) -> bool: - from adcp.reporting.outbox import _schema - + async def _schema_proven( + self, ledger: ReportingDurableNotificationLedger, *, composite: bool + ) -> bool: assert self.projector is not None wiring: tuple[object, ...] = ( self.worker, @@ -83,12 +87,9 @@ async def _schema_proven(self, pool: Any, *, composite: bool) -> bool: self.worker.cipher, self.worker.subscriptions, self.worker.signing, - pool, + ledger.storage_identity, ) - status_contract = "" if composite: - from adcp.reporting.outbox import status_schema - assert self.status is not None wiring += ( self.status, @@ -96,12 +97,10 @@ async def _schema_proven(self, pool: Any, *, composite: bool) -> bool: self.status.worker, self.status.worker.outbox, ) - status_contract = _schema._digest(status_schema.REQUIRED_STATUS_OBJECTS) key: _ProofKey = ( "B+C" if composite else "B", tuple(id(component) for component in wiring), - _schema._digest(_schema.REQUIRED_OBJECTS), - status_contract, + ledger.activity_schema_contract(composite=composite), ) state = self._schema_validation if state.positive == key: @@ -110,7 +109,9 @@ async def _schema_proven(self, pool: Any, *, composite: bool) -> bool: if flight is None: flight = _SchemaFlight(key, state.epoch) state.flight = flight - flight.task = asyncio.create_task(self._scan_schema(pool, flight, composite=composite)) + flight.task = asyncio.create_task( + self._scan_schema(ledger, flight, composite=composite) + ) flight.task.add_done_callback(_consume_schema_failure) task = flight.task if task is None or flight.key != key or task.get_loop() is not asyncio.get_running_loop(): @@ -123,27 +124,16 @@ async def _schema_proven(self, pool: Any, *, composite: bool) -> bool: # Rerun the same cheap checks before accepting the completed proof. return await self.durable() - async def _scan_schema(self, pool: Any, flight: _SchemaFlight, *, composite: bool) -> bool: - from adcp.reporting.outbox import _schema - + async def _scan_schema( + self, + ledger: ReportingDurableNotificationLedger, + flight: _SchemaFlight, + *, + composite: bool, + ) -> bool: state = self._schema_validation try: - async with pool.connection() as connection: - try: - installed = await _schema.schema_objects(connection) - except Exception: - raise ReportingNotificationError( - "notification_schema_unready:catalog_unavailable" - ) from None - # One physical catalog capture, each required contract once. - retired = await _schema.legacy_rows_retired(connection, installed) - _schema._validate_schema_objects( - installed, activity=True, **({"legacy_rows_retired": True} if retired else {}) - ) - if composite: - from adcp.reporting.outbox import status_schema - - status_schema._validate_status_objects(installed, activity=True, status=False) + await ledger.validate_activity_schema(composite=composite) if state.epoch != flight.epoch or state.flight is not flight: return False state.positive = flight.key @@ -179,71 +169,46 @@ async def durable(self) -> bool: ): raise ReportingNotificationError("activity_chain_unready") return False - # Lazy imports retain base-install operation without the [pg] extra. - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore - from adcp.reporting.outbox.pg import PgReportingOutbox - - if ( - type(self.worker.outbox) is not PgReportingOutbox - or not isinstance(self.worker.outbox, PgReportingOutbox) - or type(self.ledger) not in {PgReportingLedgerStore, PgReportingReconciliationStore} - or not isinstance(self.ledger, PgReportingLedgerStore) - or self.ledger._pool is not self.worker.outbox._pool - or not self.ledger._notifications_enabled - ): + # A durable ledger proves its own queue and storage identity. + ledger = self.ledger + if not isinstance(ledger, ReportingDurableNotificationLedger): raise ReportingNotificationError("activity_chain_unready") - return await self._schema_proven(self.ledger._pool, composite=False) + ledger.validate_activity_chain(self.worker.outbox) + return await self._schema_proven(ledger, composite=False) async def _composite_durable(self) -> bool: """Only the closed B+C read/purge union may replace the original B reader.""" assert self.status is not None if not self.status.scheduled: return False - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore - from adcp.reporting.outbox.pg import PgReportingOutbox from adcp.reporting.outbox.routing import ReportingEnvelopeCipher - from adcp.reporting.outbox.status_activity_pg import PgReportingActivityUnionStore - from adcp.reporting.outbox.status_pg import ( - PgReportingStatusOutbox, - PgStatusNotificationStore, - ) from adcp.reporting.outbox.worker import ReportingNotificationWorker store = self.status.store - if not isinstance(store, PgStatusNotificationStore): + if not isinstance(store, ReportingDurableStatusStore): return False reader = self.projector.store if self.projector is not None else None + ledger = self.ledger if ( type(self.worker) is not ReportingNotificationWorker - or type(store) is not PgStatusNotificationStore - or type(self.ledger) not in {PgReportingLedgerStore, PgReportingReconciliationStore} - or not isinstance(self.ledger, PgReportingLedgerStore) - or type(self.worker.outbox) is not PgReportingOutbox - or not isinstance(self.worker.outbox, PgReportingOutbox) + or not isinstance(ledger, ReportingDurableNotificationLedger) or type(self.worker.cipher) is not ReportingEnvelopeCipher or type(self.status.worker) is not ReportingNotificationWorker or type(self.projector) is not ReportingActivityProjector - or type(reader) is not PgReportingActivityUnionStore - or not isinstance(reader, PgReportingActivityUnionStore) - or reader.b_outbox is not self.worker.outbox - or reader.status_outbox is not store.outbox - or self.worker.activity is not self.worker.outbox - or self.status.worker.activity is not store.outbox - or self.status.worker.outbox is not store.outbox - or self.ledger is not store.ledger - or self.worker.outbox._pool is not store.ledger._pool - or type(store.outbox) is not PgReportingStatusOutbox - or store.outbox._pool is not store.ledger._pool - or self.worker.outbox._clock is not store.outbox._clock - or not store.ledger._notifications_enabled + or id(self.worker.activity) != id(self.worker.outbox) + or id(self.status.worker.activity) != id(store.outbox) + or id(self.status.worker.outbox) != id(store.outbox) + or ledger is not store.ledger or self.worker.cipher is not self.status.worker.cipher or self.worker.subscriptions is not self.status.worker.subscriptions or self.worker.signing is not self.status.worker.signing ): raise ReportingNotificationError("activity_chain_unready") - return await self._schema_proven(store.ledger._pool, composite=True) + # Exact queue, union-reader and storage identity checks are the ledger's. + ledger.validate_composite_activity_chain( + outbox=self.worker.outbox, status_store=store, reader=reader + ) + return await self._schema_proven(ledger, composite=True) async def capability_flags( self, *, account_activity: ReportingActivityProjector | None = None diff --git a/src/adcp/reporting/production/__init__.py b/src/adcp/reporting/production/__init__.py index 6c4d0fa2f..b913355b5 100644 --- a/src/adcp/reporting/production/__init__.py +++ b/src/adcp/reporting/production/__init__.py @@ -2,11 +2,11 @@ Mount one support's handler, start its owned lifecycle, then activate accounts after migration and drain. The memory implementation is for conformance and -does not claim durability. PostgreSQL uses the shared optional driver guard. +does not claim durability. The PostgreSQL production store is +``adcp_reporting_storage.production.pg``; both implement +:class:`~adcp.reporting.production.store.ReportingProductionStore`. """ -from typing import TYPE_CHECKING, Any - from adcp.reporting.production.configuration import ( ConfigurationAdmission, ConfigurationTask, @@ -34,17 +34,16 @@ ReportingProductionSupport, ) from adcp.reporting.production.source_registry import ReportingProductionSourceRegistry - -if TYPE_CHECKING: - from adcp.reporting.production.pg import PgReportingProductionOutbox, PgReportingProductionStore +from adcp.reporting.production.store import ( + ReportingProductionProjection, + ReportingProductionStore, +) __all__ = [ "ConfigurationAdmission", "ConfigurationTask", "InMemoryReportingProductionOutbox", "InMemoryReportingProductionStore", - "PgReportingProductionOutbox", - "PgReportingProductionStore", "ReportingConfigurationAdmission", "ReportingProductionConfigurationTask", "ReportingProductionDestination", @@ -52,18 +51,12 @@ "ReportingProductionHandler", "ReportingProductionMethod", "ReportingProductionOffering", + "ReportingProductionProjection", "ReportingProductionSigning", "ReportingProductionSource", "ReportingProductionSourceBinding", "ReportingProductionSourceRegistry", + "ReportingProductionStore", "ReportingProductionSupport", "production_notification_workers", ] - - -def __getattr__(name: str) -> Any: - if name in {"PgReportingProductionOutbox", "PgReportingProductionStore"}: - from adcp.reporting.production import pg - - return getattr(pg, name) - raise AttributeError(name) diff --git a/src/adcp/reporting/production/delivery_window.py b/src/adcp/reporting/production/delivery_window.py index 244e0cd1e..f52760c56 100644 --- a/src/adcp/reporting/production/delivery_window.py +++ b/src/adcp/reporting/production/delivery_window.py @@ -3,67 +3,53 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from datetime import datetime from adcp.reporting.evidence import aware_utc from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.outbox.activity import ActivityRequest, ReportingActivityStore, WebhookAttempt -from adcp.reporting.outbox.memory import InMemoryReportingOutbox from adcp.reporting.outbox.models import DeliveryLease -from adcp.reporting.production.memory import InMemoryReportingProductionStore - -if TYPE_CHECKING: - from adcp.reporting.production.pg import PgReportingProductionStore +from adcp.reporting.production.store import ReportingProductionStore, SavedDeliveryWindow RETRY_HORIZON_SECONDS = 86400 +def inspect_window( + saved: SavedDeliveryWindow | None, + expected: tuple[str, str], + moment: datetime, +) -> tuple[datetime | None, bool]: + """The recorded deadline and whether ``moment`` has passed it. + + A window recorded for another queue or body, or one that starts after + ``moment``, is a retry contract violation rather than an expiry. + """ + if saved is None: + return None, False + if saved[:2] != expected or moment < saved[2]: + raise ReportingNotificationError("notification_retry_unready") + return saved[3], moment >= saved[3] + + @dataclass(frozen=True) class ProductionDeliveryWindow: - store: InMemoryReportingProductionStore | PgReportingProductionStore + """One queue's window over the production store that records it. + + The store runs the reservation transaction (its lock, its clock, its + activity participant); this object owns the queue identity and the policy. + """ + + store: ReportingProductionStore queue: str - @staticmethod - def _inspect( - saved: tuple[str, str, datetime, datetime] | None, - expected: tuple[str, str], - moment: datetime, - ) -> tuple[datetime | None, bool]: - if saved is None: - return None, False - if saved[:2] != expected or moment < saved[2]: - raise ReportingNotificationError("notification_retry_unready") - return saved[3], moment >= saved[3] + _inspect = staticmethod(inspect_window) async def inspect(self, lease: DeliveryLease, *, now: datetime) -> tuple[datetime | None, bool]: binding = lease.delivery.binding key = (binding.account_id, binding.idempotency_key) expected = (self.queue, binding.body_sha256) - if isinstance(self.store, InMemoryReportingProductionStore): - async with self.store._lock: - return self._inspect( - self.store._production_delivery_windows.get(key), expected, aware_utc(now) - ) - from adcp.reporting.outbox.pg import database_now - - try: - async with self.store._connection() as connection, connection.transaction(): - saved = await ( - await connection.execute( - "SELECT queue,body_sha256,started_at,expires_at" - " FROM reporting_production_delivery_windows" - " WHERE account_id=%s AND idempotency_key=%s", - key, - ) - ).fetchone() - return self._inspect( - saved, expected, await database_now(connection, self.store._clock) - ) - except ReportingNotificationError: - raise - except Exception: - raise ReportingNotificationError("notification_retry_unready") from None + saved, moment = await self.store.read_delivery_window(key, now=aware_utc(now)) + return inspect_window(saved, expected, moment) async def reserve_attempt( self, @@ -80,81 +66,14 @@ async def reserve_attempt( attempt together. Unknown commit outcomes leave the original identity. """ binding = lease.delivery.binding - key = (binding.account_id, binding.idempotency_key) - expected = (self.queue, binding.body_sha256) if self.queue not in {"core", "status", "ready"}: raise ReportingNotificationError("notification_retry_unready") - if isinstance(self.store, InMemoryReportingProductionStore): - if ( - not isinstance(activity, InMemoryReportingOutbox) - or activity._store is not self.store - ): - raise ReportingNotificationError("notification_retry_unready") - async with self.store._mutation(): - at = aware_utc(now) - saved = self.store._production_delivery_windows.get(key) - deadline, expired = self._inspect(saved, expected, at) - if expired: - return None, deadline, True - attempt = activity._reserve_attempt_locked(lease, request=request, now=at) - if attempt is None: - return None, deadline, False - retained = self.store._production_delivery_windows.setdefault( - key, - ( - *expected, - attempt.fired_at, - attempt.fired_at + timedelta(seconds=RETRY_HORIZON_SECONDS), - ), - ) - return attempt, retained[3], False - - from adcp.reporting.outbox.pg import PgReportingOutbox, database_now - - if not isinstance(activity, PgReportingOutbox) or activity._pool is not self.store._pool: - raise ReportingNotificationError("notification_retry_unready") - try: - # The activity participant supplies its exact queue adapter and - # connection. Take the inherited account lock before its row lock. - async with activity._activity_transaction() as connection: - await self.store._lock_account(connection, binding.account_id) - moment = await database_now(connection, self.store._clock) - saved = await ( - await connection.execute( - "SELECT queue,body_sha256,started_at,expires_at" - " FROM reporting_production_delivery_windows" - " WHERE account_id=%s AND idempotency_key=%s", - key, - ) - ).fetchone() - deadline, expired = self._inspect(saved, expected, moment) - if expired: - return None, deadline, True - attempt = await activity._reserve_attempt_on(connection, lease, request=request) - if attempt is None: - return None, deadline, False - deadline, expired = self._inspect(saved, expected, attempt.fired_at) - if expired: - # Time can cross the deadline while reserving an ordinal. - # Roll back that provisional head and attempt, too. - raise ReportingNotificationError("notification_retry_expired") - await connection.execute( - "INSERT INTO reporting_production_delivery_windows" - " (account_id,idempotency_key,queue,body_sha256,started_at,expires_at)" - " VALUES(%s,%s,%s,%s,%s,%s) ON CONFLICT DO NOTHING", - ( - *key, - *expected, - attempt.fired_at, - attempt.fired_at + timedelta(seconds=RETRY_HORIZON_SECONDS), - ), - ) - return ( - attempt, - deadline or attempt.fired_at + timedelta(seconds=RETRY_HORIZON_SECONDS), - False, - ) - except ReportingNotificationError as error: - if error.code == "notification_retry_expired": - return None, deadline, True - raise + return await self.store.reserve_delivery_attempt( + activity, + lease, + request=request, + now=aware_utc(now), + queue=self.queue, + expected=(self.queue, binding.body_sha256), + horizon_seconds=RETRY_HORIZON_SECONDS, + ) diff --git a/src/adcp/reporting/production/memory.py b/src/adcp/reporting/production/memory.py index 7ac0770e2..cd8898482 100644 --- a/src/adcp/reporting/production/memory.py +++ b/src/adcp/reporting/production/memory.py @@ -4,8 +4,9 @@ import json import weakref +from collections.abc import Sequence from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any from adcp.reporting.canonical_json import canonical_json_utf8_v1 @@ -17,6 +18,7 @@ from adcp.reporting.ledger.models import ( ReportingConfiguration, ReportingConfigurationGenerationKey, + ReportingDeliveryEscalation, ReportingObligationRecord, ) from adcp.reporting.ledger.notification_models import ( @@ -28,10 +30,21 @@ from adcp.reporting.materializer.capture import ReportingMaterializerBoundary from adcp.reporting.materializer.contracts import ReportingVerificationKey, failure from adcp.reporting.materializer.work import MaterializerContext, ReportingMaterializerLease +from adcp.reporting.outbox.activity import ActivityRequest, ReportingActivityStore, WebhookAttempt from adcp.reporting.outbox.memory import InMemoryReportingOutbox, NotificationState +from adcp.reporting.outbox.models import DeliveryLease from adcp.reporting.production.contracts import ReportingProductionSourceBinding from adcp.reporting.production.source_registry import ReportingProductionSourceContext -from adcp.reporting.projection.memory import InMemoryReportingProjectionStore +from adcp.reporting.production.store import ( + MaintenanceOperation, + ReportingProductionProjection, + SavedDeliveryWindow, +) +from adcp.reporting.projection.memory import ( + InMemoryReportingProjectionOutbox, + InMemoryReportingProjectionStore, + InMemoryReportingStatusProjection, +) from adcp.reporting.source import ReportingConstituent if TYPE_CHECKING: @@ -72,7 +85,7 @@ class InMemoryReportingProductionStore(InMemoryReportingProjectionStore): """Matches production atomicity and epochs, without claiming durable storage.""" _materializer_writer_epoch = 2 - _production_support: weakref.ReferenceType[ReportingProductionSupport] | None = None + production_support: weakref.ReferenceType[ReportingProductionSupport] | None = None def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -101,6 +114,117 @@ def _owner(self) -> ReportingProductionSupport: return production_owner(self) + # -- the production store contract (adcp.reporting.production.store) ------ + + @property + def durable(self) -> bool: + return False + + @property + def storage_identity(self) -> object: + return self + + @property + def storage_open(self) -> bool: + return True + + def is_sdk_production_store(self) -> bool: + return type(self) is InMemoryReportingProductionStore + + def status_projection( + self, + *, + consumer_status_enabled: bool, + escalation: ReportingDeliveryEscalation | None, + revision_ownership: bool, + ) -> InMemoryReportingStatusProjection: + return InMemoryReportingStatusProjection( + self, + consumer_status_enabled=consumer_status_enabled, + escalation=escalation, + revision_ownership=revision_ownership, + ) + + def is_sdk_status_projection(self, projection: object) -> bool: + return type(projection) is InMemoryReportingStatusProjection + + def projection_linked(self, projection: ReportingProductionProjection) -> bool: + return ( + type(projection.outbox) is InMemoryReportingProjectionOutbox + and projection.outbox._store is self + ) + + def production_outboxes( + self, projection: ReportingProductionProjection + ) -> tuple[Any, Any, Any]: + if type(self) is not InMemoryReportingProductionStore: + raise ReportingNotificationError("notification_chain_unready") + return ( + InMemoryReportingOutbox(self), + projection.outbox, + InMemoryReportingProductionOutbox(self), + ) + + def notification_workers_linked(self, workers: Sequence[Any]) -> bool: + expected = ( + InMemoryReportingOutbox, + InMemoryReportingProjectionOutbox, + InMemoryReportingProductionOutbox, + ) + return ( + all(getattr(w.outbox, "_store", None) is self for w in workers) + and tuple(type(w.outbox) for w in workers) == expected + ) + + async def validate_production_schema( + self, *, notifications: bool, service_context: bool + ) -> None: + """Memory has no catalog; it never advertises durability either.""" + + async def read_delivery_window( + self, key: tuple[str, str], *, now: datetime + ) -> tuple[SavedDeliveryWindow | None, datetime]: + async with self._lock: + return self._production_delivery_windows.get(key), now + + async def reserve_delivery_attempt( + self, + activity: ReportingActivityStore, + lease: DeliveryLease, + *, + request: ActivityRequest, + now: datetime, + queue: str, + expected: tuple[str, str], + horizon_seconds: int, + ) -> tuple[WebhookAttempt | None, datetime | None, bool]: + from adcp.reporting.production.delivery_window import inspect_window + + binding = lease.delivery.binding + key = (binding.account_id, binding.idempotency_key) + if not isinstance(activity, InMemoryReportingOutbox) or activity._store is not self: + raise ReportingNotificationError("notification_retry_unready") + async with self._mutation(): + saved = self._production_delivery_windows.get(key) + deadline, expired = inspect_window(saved, expected, now) + if expired: + return None, deadline, True + attempt = activity._reserve_attempt_locked(lease, request=request, now=now) + if attempt is None: + return None, deadline, False + retained = self._production_delivery_windows.setdefault( + key, + ( + *expected, + attempt.fired_at, + attempt.fired_at + timedelta(seconds=horizon_seconds), + ), + ) + return attempt, retained[3], False + + def maintenance_operations(self) -> tuple[MaintenanceOperation, ...]: + return () + async def admit_production_configuration( self, configuration: ReportingConfiguration, @@ -272,7 +396,7 @@ async def finish_producer_acquisition( work.obligation, work.turn, acquisition_state(work.obligation, revisions) ) - async def _activate_production(self, *, account_id: str) -> bool: + async def activate_production(self, *, account_id: str) -> bool: owner = self._owner() async with self._mutation(): projection = self._projection_accounts.get(account_id) diff --git a/src/adcp/reporting/production/notifications.py b/src/adcp/reporting/production/notifications.py index 1b2224cb3..768cd4a1e 100644 --- a/src/adcp/reporting/production/notifications.py +++ b/src/adcp/reporting/production/notifications.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from datetime import datetime +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable import httpx from adcp.reporting.ledger.notification_models import ReportingNotificationError -from adcp.reporting.outbox.memory import InMemoryReportingOutbox from adcp.reporting.outbox.routing import ( ReportingEnvelopeCipher, ReportingNotificationSubscription, @@ -21,17 +21,14 @@ RETRY_HORIZON_SECONDS, ProductionDeliveryWindow, ) -from adcp.reporting.production.memory import ( - InMemoryReportingProductionOutbox, - InMemoryReportingProductionStore, +from adcp.reporting.production.store import ( + ReportingProductionProjection, + ReportingProductionStore, ) -from adcp.reporting.projection.memory import InMemoryReportingStatusProjection from adcp.signing.crypto import ALLOWED_ALGS if TYPE_CHECKING: - from adcp.reporting.production.pg import PgReportingProductionStore from adcp.reporting.production.service import ReportingProductionSupport - from adcp.reporting.projection.pg import PgReportingStatusProjection @dataclass(frozen=True) @@ -126,8 +123,8 @@ async def get_active( def production_notification_workers( - store: InMemoryReportingProductionStore | PgReportingProductionStore, - projection: InMemoryReportingStatusProjection | PgReportingStatusProjection, + store: ReportingProductionStore, + projection: ReportingProductionProjection, *, subscriptions: ReportingSubscriptionResolver, cipher: ReportingEnvelopeCipher, @@ -139,32 +136,17 @@ def production_notification_workers( and enabled atomic logical enqueue available, without advertising push. Retained epoch-zero readiness queues are never among these participants. """ - from adcp.reporting.outbox.pg import PgReportingOutbox - from adcp.reporting.production.pg import PgReportingProductionOutbox, PgReportingProductionStore - if ( - projection.ledger is not store + not isinstance(store, ReportingProductionStore) + or projection.ledger is not store or not projection.policy["notifications_enabled"] or type(signing) is not ReportingProductionSigning ): raise ReportingNotificationError("notification_chain_unready") ReportingProductionSigning.__post_init__(signing) signed_subscriptions = _SignedSubscriptions(subscriptions) - outboxes: tuple[Any, ...] - if type(store) is InMemoryReportingProductionStore: - outboxes = ( - InMemoryReportingOutbox(store), - projection.outbox, - InMemoryReportingProductionOutbox(store), - ) - elif type(store) is PgReportingProductionStore: - outboxes = ( - PgReportingOutbox(pool=store._pool, clock=store._clock), - projection.outbox, - PgReportingProductionOutbox(pool=store._pool, clock=store._clock), - ) - else: - raise ReportingNotificationError("notification_chain_unready") + # The store builds its own exact queue classes; a subclass is refused there. + outboxes = store.production_outboxes(projection) return tuple( ReportingNotificationWorker( outbox=outbox, @@ -172,7 +154,7 @@ def production_notification_workers( cipher=cipher, signing=signing, activity=outbox, - clock=store._clock, + clock=store.clock, delivery_window=ProductionDeliveryWindow(store, queue), ) for outbox, queue in zip(outboxes, ("core", "status", "ready")) @@ -222,24 +204,7 @@ def check_workers(support: ReportingProductionSupport) -> None: or tuple(worker_identity(w) for w in workers) != support._notification_identity ): raise ReportingNotificationError("notification_chain_unready") - expected: tuple[type[Any], ...] - if isinstance(support.store, InMemoryReportingProductionStore): - from adcp.reporting.projection.memory import InMemoryReportingProjectionOutbox - - expected = ( - InMemoryReportingOutbox, - InMemoryReportingProjectionOutbox, - InMemoryReportingProductionOutbox, - ) - linked = all(getattr(w.outbox, "_store", None) is support.store for w in workers) - else: - from adcp.reporting.outbox.pg import PgReportingOutbox - from adcp.reporting.production.pg import PgReportingProductionOutbox - from adcp.reporting.projection.notifications import PgReportingProjectionOutbox - - expected = (PgReportingOutbox, PgReportingProjectionOutbox, PgReportingProductionOutbox) - linked = all(getattr(w.outbox, "_pool", None) is support.store._pool for w in workers) - if not linked or tuple(type(w.outbox) for w in workers) != expected: + if not support.store.notification_workers_linked(workers): raise ReportingNotificationError("notification_chain_unready") for worker, queue in zip(workers, ("core", "status", "ready")): window = worker.delivery_window @@ -319,38 +284,17 @@ async def check_account_notifications(support: ReportingProductionSupport, accou check_workers(support) +@runtime_checkable +class _DueAccountQueue(Protocol): + async def next_due_account(self, *, delivery: bool, now: datetime) -> str | None: ... + + async def next_account(worker: ReportingNotificationWorker, *, delivery: bool) -> str | None: """Sample one indexed due queue row; never enumerate adopter accounts.""" - from adcp.reporting.outbox.pg import PgReportingOutbox, database_now - outbox = worker.outbox - if isinstance(outbox, InMemoryReportingOutbox): - now = worker._clock() - async with outbox._store._lock: - pending = ( - ((key[0], work) for key, (_, work) in outbox._state.deliveries.items()) - if delivery - else ((key[0], work) for key, work in outbox._state.expansions.items()) - ) - values = [(work.due_at, account) for account, work in pending if work.available(now)] - return min(values)[1] if values else None - if not isinstance(outbox, PgReportingOutbox): + if not isinstance(outbox, _DueAccountQueue): raise ReportingNotificationError("notification_chain_unready") - # Both identifiers are closed SDK literals. The concrete queue adapter - # selects the matching isolated table on this same actual connection. - table = "reporting_notification_deliveries" if delivery else "reporting_notification_expansions" - async with outbox._connection() as connection: - now = await database_now(connection, outbox._clock) - row = await ( - await connection.execute( - f"SELECT account_id FROM {table}" # nosec B608 - " WHERE due_at<=%s AND (state='pending' OR" - " (state='leased' AND lease_expires_at<=%s))" - " ORDER BY due_at,account_id LIMIT 1", - (now, now), - ) - ).fetchone() - return str(row[0]) if row is not None else None + return await outbox.next_due_account(delivery=delivery, now=worker._clock()) async def notification_turn(support: ReportingProductionSupport) -> None: diff --git a/src/adcp/reporting/production/service.py b/src/adcp/reporting/production/service.py index fbbbf61d6..d3600c997 100644 --- a/src/adcp/reporting/production/service.py +++ b/src/adcp/reporting/production/service.py @@ -39,21 +39,21 @@ ReportingProductionMethod, ReportingProductionSourceBinding, ) -from adcp.reporting.production.memory import InMemoryReportingProductionStore from adcp.reporting.production.offerings import ReportingProductionOffering from adcp.reporting.production.source_registry import ( ReportingProductionSourceContext, ReportingProductionSourceRegistry, ) -from adcp.reporting.projection.memory import InMemoryReportingStatusProjection +from adcp.reporting.production.store import ( + ReportingProductionProjection, + ReportingProductionStore, +) from adcp.reporting.receipts.handler import ReceiptAccountResolver if TYPE_CHECKING: from adcp.decisioning.registry import BuyerAgentRegistry from adcp.reporting.ledger.producer import ReportingProducer from adcp.reporting.outbox.worker import ReportingNotificationWorker - from adcp.reporting.production.pg import PgReportingProductionStore - from adcp.reporting.projection.pg import PgReportingStatusProjection from adcp.reporting.service_lifecycle import _ServiceLifecycle from adcp.server.base import ADCPHandler @@ -98,10 +98,8 @@ def _method_ready( ) -def production_owner( - store: InMemoryReportingProductionStore | PgReportingProductionStore, -) -> ReportingProductionSupport: - reference = store._production_support +def production_owner(store: ReportingProductionStore) -> ReportingProductionSupport: + reference = store.production_support owner = reference() if reference is not None else None if owner is None or owner.store is not store: raise ReportingNotificationError("reporting_production_component_unready") @@ -161,7 +159,7 @@ class ReportingProductionSupport: def __init__( self, materializer: ReportingMaterializerService, - projection: InMemoryReportingStatusProjection | PgReportingStatusProjection, + projection: ReportingProductionProjection, *, offerings: tuple[ReportingProductionOffering, ...], configuration_task: ReportingProductionConfigurationTask, @@ -177,16 +175,14 @@ def __init__( ) -> None: from adcp.reporting.outbox.worker import ReportingNotificationWorker from adcp.reporting.production.handler import ReportingProductionHandler - from adcp.reporting.production.pg import PgReportingProductionStore - from adcp.reporting.projection.pg import PgReportingStatusProjection + store = materializer.store if ( type(materializer) is not ReportingMaterializerService - or type(materializer.store) - not in {InMemoryReportingProductionStore, PgReportingProductionStore} - or type(projection) - not in {InMemoryReportingStatusProjection, PgReportingStatusProjection} - or projection.ledger is not materializer.store + or not isinstance(store, ReportingProductionStore) + or not store.is_sdk_production_store() + or not store.is_sdk_status_projection(projection) + or projection.ledger is not store or type(configuration_task) is not ReportingProductionConfigurationTask or type(offerings) is not tuple or not offerings @@ -203,11 +199,9 @@ def __init__( raise ValueError( "production support requires exact SDK components and bounded promises" ) - assert isinstance( - materializer.store, (InMemoryReportingProductionStore, PgReportingProductionStore) - ) - self.store = materializer.store - previous = self.store._production_support + assert isinstance(store, ReportingProductionStore) + self.store: ReportingProductionStore = store + previous = self.store.production_support if previous is not None and previous() is not None: raise ReportingNotificationError("reporting_production_owner_conflict") self.materializer, self.projection = materializer, projection @@ -253,7 +247,7 @@ def __init__( self._protocol_version = self.handler.get_adcp_version() self._components = self._component_ids() self._methods = self._handler_methods() - self.store._production_support = weakref.ref(self) + self.store.production_support = weakref.ref(self) self._assert_components(running=False, mounted=False) from adcp.reporting.production._declaration import declared_reporting_delivery from adcp.reporting.production.notifications import signing_capabilities, signing_identity @@ -264,7 +258,7 @@ def __init__( "reporting_delivery": declared_reporting_delivery( offerings=[o.wire() for o in self.offerings], destination=writer, - durable=not isinstance(self.store, InMemoryReportingProductionStore), + durable=self.store.durable, escalation=self.projection.escalation or ReportingDeliveryEscalation(), consumer_status_enabled=self.projection.consumer_status_enabled, automated_recovery_window=self.automated_recovery_window, @@ -301,7 +295,7 @@ def _component_ids(self) -> tuple[int, ...]: id(v) for v in ( self.store, - getattr(self.store, "_pool", None), + self.store.storage_identity, self.materializer, self.materializer.io, self.materializer.io.registry, @@ -366,18 +360,14 @@ def _assert_components(self, *, running: bool = True, mounted: bool = True) -> N or self._methods != self._handler_methods() or type(self.handler._adcp_version) is not str or self.handler._adcp_version != self._protocol_version - or ( - (running or mounted) - and not isinstance(self.store, InMemoryReportingProductionStore) - and self.store._pool.closed is not False - ) + or ((running or mounted) and not self.store.storage_open) or self.materializer.store is not self.store or self.projection.ledger is not self.store - or not self._projection_outbox_linked() + or not self.store.projection_linked(self.projection) or self.handler.receipt_store is not self.store or self.handler.reporting_feed_store is not self.store or self.handler._feed_consumer_status_enabled != self.projection.consumer_status_enabled - or self.store._projection_read_policy != self.projection.policy + or self.store.projection_read_policy != self.projection.policy or type(self.materializer.io) is not ReportingDestinationIO or isinstance(writer, ReferenceReportingDestinationWriter) or isinstance(self.materializer.io.resolver, ReferenceReportingResolver) @@ -392,24 +382,6 @@ def _assert_components(self, *, running: bool = True, mounted: bool = True) -> N ): raise ReportingNotificationError("reporting_production_component_unready") - def _projection_outbox_linked(self) -> bool: - # The projection queue participates even when optional HTTP delivery - # workers are absent. Its connection ownership cannot ride a cached - # catalog proof belonging to a different pool or in-memory ledger. - if isinstance(self.store, InMemoryReportingProductionStore): - from adcp.reporting.projection.memory import InMemoryReportingProjectionOutbox - - return ( - type(self.projection.outbox) is InMemoryReportingProjectionOutbox - and self.projection.outbox._store is self.store - ) - from adcp.reporting.projection.notifications import PgReportingProjectionOutbox - - return ( - type(self.projection.outbox) is PgReportingProjectionOutbox - and self.projection.outbox._pool is self.store._pool - ) - def _offering_ready(self, offering: ReportingProductionOffering) -> bool: try: if ( @@ -617,14 +589,11 @@ def invalidate_schema_validation(self) -> None: self._schema_task = None async def _schema_ready(self) -> bool: - from adcp.reporting.production.pg import PgReportingProductionStore - from adcp.reporting.production.schema import validate_production_schema - - if not isinstance(self.store, PgReportingProductionStore): - return False store = self.store - pool, epoch = store._pool, self._schema_epoch - identity = (id(pool), epoch) + if not store.durable: + return False + storage, epoch = store.storage_identity, self._schema_epoch + identity = (id(storage), epoch) if self._schema_positive == identity: return True task = self._schema_task @@ -632,13 +601,11 @@ async def _schema_ready(self) -> bool: async def scan() -> bool: try: - async with pool.connection() as connection: - await validate_production_schema( - connection, - notifications=self.notifications_enabled, - service_context=self.source_registry is not None, - ) - if epoch == self._schema_epoch and pool is store._pool: + await store.validate_production_schema( + notifications=self.notifications_enabled, + service_context=self.source_registry is not None, + ) + if epoch == self._schema_epoch and storage is store.storage_identity: self._schema_positive = identity return True return False @@ -658,10 +625,7 @@ async def start(self) -> None: self._assert_components() return self._assert_components(running=False) - if ( - not isinstance(self.store, InMemoryReportingProductionStore) - and not await self._schema_ready() - ): + if self.store.durable and not await self._schema_ready(): raise ReportingNotificationError("reporting_production_schema_unready") self._assert_components(running=False) self._task = asyncio.create_task(self._run(), name="adcp-reporting-production") @@ -731,20 +695,7 @@ async def _maintain(self) -> None: _maintenance_stalled, ) - store: Any = self.store - operations: list[tuple[str, Any]] = [] - if getattr(store, "_row_storage", None) is not None: - operations.append(("intent_sweep", lambda: store.sweep_row_write_intents(limit=100))) - # Frees row bytes of periods past retention even while their records stay (protected - # by append-only guards), so storage is reclaimed whether or not records can be. - operations.append(("row_pruning", lambda: store.prune_expired_row_sets(limit=20))) - if getattr(store, "_row_storage", None) is not None or getattr( - store, "_change_feed", False - ): - operations.append(("period_retirement", lambda: store.retire_expired_periods(limit=20))) - if getattr(store, "_change_feed", False): - operations.append(("change_pruning", lambda: store.prune_changes())) - for name, run in operations: + for name, run in self.store.maintenance_operations(): try: result = await run() except asyncio.CancelledError: @@ -810,7 +761,7 @@ async def _check_notifications(self, account_id: str) -> None: async def activate(self, *, account_id: str) -> bool: await self._check_notifications(account_id) await self.projection.activate(account_id=account_id) - return await self.store._activate_production(account_id=account_id) + return await self.store.activate_production(account_id=account_id) async def aclose(self) -> None: self._stop.set() @@ -827,8 +778,8 @@ async def aclose(self) -> None: proof = self._schema_task if proof is not None: await asyncio.gather(proof, return_exceptions=True) - if self.store._production_support is not None and self.store._production_support() is self: - self.store._production_support = None + if self.store.production_support is not None and self.store.production_support() is self: + self.store.production_support = None async def reporting_delivery(self, *, extra: Mapping[str, Any] | None = None) -> dict[str, Any]: # Validate protected extra even when the concrete surface is unready. diff --git a/src/adcp/reporting/production/store.py b/src/adcp/reporting/production/store.py new file mode 100644 index 000000000..398d40873 --- /dev/null +++ b/src/adcp/reporting/production/store.py @@ -0,0 +1,182 @@ +"""The store contract the production composition depends on. + +:class:`~adcp.reporting.production.service.ReportingProductionSupport` composes +a materializer, a status projection and notification workers over one store. +It used to recognize the two shipped stores by class. The in-memory store now +lives in ``adcp`` and the PostgreSQL store in ``adcp-reporting-storage``, so +the composition asks the store instead: which projection and queues belong to +it, whether its schema is installed, what maintenance it needs. Both shipped +stores keep the exact-class checks inside their own answers, so an adopter +subclass is still refused where it was refused before. +""" + +from __future__ import annotations + +import weakref +from collections.abc import Awaitable, Callable, Sequence +from datetime import datetime +from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable + +from adcp.reporting.feed.store import ReportingFeedStore +from adcp.reporting.ledger.delivery import ReportingReconciliationStore +from adcp.reporting.ledger.store import ReportingLedgerStore +from adcp.reporting.materializer.work import ReportingMaterializerStore +from adcp.reporting.receipts.store import ReportingReceiptBatchStore + +if TYPE_CHECKING: + from adcp.reporting.ledger.delivery_models import ReportingDestinationBinding + from adcp.reporting.ledger.models import ReportingConfiguration, ReportingDeliveryEscalation + from adcp.reporting.outbox.activity import ( + ActivityRequest, + ReportingActivityStore, + WebhookAttempt, + ) + from adcp.reporting.outbox.models import DeliveryLease + from adcp.reporting.outbox.status import StatusTurn + from adcp.reporting.outbox.worker import ReportingNotificationWorker + from adcp.reporting.production.service import ReportingProductionSupport + from adcp.reporting.production.source_registry import ReportingProductionSourceContext + +__all__ = [ + "MaintenanceOperation", + "ReportingProductionProjection", + "ReportingProductionStore", + "SavedDeliveryWindow", +] + +#: ``(queue, body_sha256, started_at, expires_at)`` of a recorded first attempt. +SavedDeliveryWindow = tuple[str, str, datetime, datetime] +#: A named, bounded, resumable retention pass the production worker schedules. +MaintenanceOperation = tuple[str, Callable[[], Awaitable[Any]]] + + +@runtime_checkable +class ReportingProductionProjection(Protocol): + """The v2 status projection a production store builds over itself.""" + + ledger: Any + outbox: Any + escalation: ReportingDeliveryEscalation | None + consumer_status_enabled: bool + + @property + def policy(self) -> dict[str, Any]: ... + + async def rebuild_one(self) -> StatusTurn: ... + + async def sweep_one(self) -> StatusTurn: ... + + async def activate(self, *, account_id: str) -> bool: ... + + +@runtime_checkable +class ReportingProductionStore( + ReportingLedgerStore, + ReportingReconciliationStore, + ReportingMaterializerStore, + ReportingReceiptBatchStore, + ReportingFeedStore, + Protocol, +): + """A ledger that can carry the production composition. + + It is a Core ledger, a reconciliation store, a materializer store, a + receipt-batch store and a frozen feed store at once, plus the composition + hooks below. + """ + + #: Set by :class:`ReportingProductionSupport` when it takes ownership of the store. + production_support: weakref.ReferenceType[ReportingProductionSupport] | None + #: Set by the status projection; the composition checks it matches its policy. + projection_read_policy: dict[str, Any] + + @property + def durable(self) -> bool: + """Whether storage survives the process. Only durable stores advertise production.""" + ... + + @property + def clock(self) -> Callable[[], datetime] | None: + """The explicit clock override, or ``None`` for the store's own time source.""" + ... + + @property + def storage_identity(self) -> object: + """The resource shared by every participant (a pool, or the store itself).""" + ... + + @property + def storage_open(self) -> bool: + """Whether that resource still accepts work.""" + ... + + def is_sdk_production_store(self) -> bool: + """Exactly a shipped production store, not a subclass.""" + ... + + def status_projection( + self, + *, + consumer_status_enabled: bool, + escalation: ReportingDeliveryEscalation | None, + revision_ownership: bool, + ) -> ReportingProductionProjection: ... + + def is_sdk_status_projection(self, projection: object) -> bool: + """Exactly the shipped projection class for this store.""" + ... + + def projection_linked(self, projection: ReportingProductionProjection) -> bool: + """Whether the projection's queue shares this store's storage.""" + ... + + def production_outboxes( + self, projection: ReportingProductionProjection + ) -> tuple[Any, Any, Any]: + """The core, status and readiness queues, in that order.""" + ... + + def notification_workers_linked(self, workers: Sequence[ReportingNotificationWorker]) -> bool: + """Whether every worker drives this store's exact queue classes over its storage.""" + ... + + async def validate_production_schema( + self, *, notifications: bool, service_context: bool + ) -> None: + """Raise ``reporting_production_schema_unready`` unless the catalog is installed.""" + ... + + async def read_delivery_window( + self, key: tuple[str, str], *, now: datetime + ) -> tuple[SavedDeliveryWindow | None, datetime]: + """The recorded window for ``(account_id, idempotency_key)`` and the store's instant.""" + ... + + async def reserve_delivery_attempt( + self, + activity: ReportingActivityStore, + lease: DeliveryLease, + *, + request: ActivityRequest, + now: datetime, + queue: str, + expected: tuple[str, str], + horizon_seconds: int, + ) -> tuple[WebhookAttempt | None, datetime | None, bool]: + """Reserve one HTTP attempt and its immutable first-attempt deadline atomically.""" + ... + + def maintenance_operations(self) -> tuple[MaintenanceOperation, ...]: + """Retention passes for the features this store opted into, in run order.""" + ... + + async def activate_production(self, *, account_id: str) -> bool: ... + + async def admit_production_configuration( + self, + configuration: ReportingConfiguration, + binding: ReportingDestinationBinding, + *, + offering_id: str, + service_context: ReportingProductionSourceContext | None = None, + ) -> None: ... diff --git a/src/adcp/reporting/projection/__init__.py b/src/adcp/reporting/projection/__init__.py index 22823c9b0..6cbdd1fa1 100644 --- a/src/adcp/reporting/projection/__init__.py +++ b/src/adcp/reporting/projection/__init__.py @@ -1,6 +1,8 @@ -"""Versioned private reporting projection, independent of optional delivery workers.""" +"""Versioned private reporting projection, independent of optional delivery workers. -from typing import TYPE_CHECKING, Any +The PostgreSQL projection store and status projection are in +``adcp_reporting_storage.projection``. +""" from adcp.reporting.projection.capture import ReportingProjectionInput from adcp.reporting.projection.memory import ( @@ -9,28 +11,9 @@ InMemoryReportingStatusProjection, ) -if TYPE_CHECKING: - from adcp.reporting.projection.notifications import PgReportingProjectionOutbox - from adcp.reporting.projection.pg import PgReportingProjectionStore, PgReportingStatusProjection - __all__ = [ "InMemoryReportingProjectionOutbox", "InMemoryReportingProjectionStore", "InMemoryReportingStatusProjection", - "PgReportingProjectionOutbox", - "PgReportingProjectionStore", - "PgReportingStatusProjection", "ReportingProjectionInput", ] - - -def __getattr__(name: str) -> Any: - if name in {"PgReportingProjectionStore", "PgReportingStatusProjection"}: - from adcp.reporting.projection import pg - - return getattr(pg, name) - if name == "PgReportingProjectionOutbox": - from adcp.reporting.projection.notifications import PgReportingProjectionOutbox - - return PgReportingProjectionOutbox - raise AttributeError(name) diff --git a/src/adcp/reporting/projection/memory.py b/src/adcp/reporting/projection/memory.py index 258b04734..4146ac3d5 100644 --- a/src/adcp/reporting/projection/memory.py +++ b/src/adcp/reporting/projection/memory.py @@ -78,8 +78,10 @@ def _state(self) -> NotificationState: class InMemoryReportingProjectionStore(InMemoryReportingFeedStore): """Reference semantics, never a production durability claim.""" - _projection_read_policy: dict[str, Any] - _projection_read_escalation: ReportingDeliveryEscalation | None + # Replaced (never mutated) by the status projection that reads this store; + # present from construction so the production store protocol matches. + projection_read_policy: dict[str, Any] = {} + projection_read_escalation: ReportingDeliveryEscalation | None = None def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -170,8 +172,8 @@ def __init__( if ledger._status_notification_state is None: ledger._status_notification_state = _StatusMemoryState() self.outbox = InMemoryReportingProjectionOutbox(ledger) - ledger._projection_read_policy = self.policy - ledger._projection_read_escalation = escalation + ledger.projection_read_policy = self.policy + ledger.projection_read_escalation = escalation def _enqueue_status(self, event: ReportingDomainEvent) -> None: self.outbox._state.enqueue(event) diff --git a/src/adcp/reporting/projection/wire.py b/src/adcp/reporting/projection/wire.py index b0272dcff..497d5540c 100644 --- a/src/adcp/reporting/projection/wire.py +++ b/src/adcp/reporting/projection/wire.py @@ -51,7 +51,7 @@ def render_tier_status( consumer_status_enabled: bool, ) -> dict[str, Any]: if ( - getattr(store, "_projection_read_policy", None) != policy + getattr(store, "projection_read_policy", None) != policy or policy["consumer_status_enabled"] != consumer_status_enabled ): raise LedgerConflictError( @@ -60,7 +60,7 @@ def render_tier_status( return ReportingStatusHandler( store, consumer_status_enabled=consumer_status_enabled, - escalation=getattr(store, "_projection_read_escalation", None), + escalation=getattr(store, "projection_read_escalation", None), ).render_snapshot( request, caller=ReportingStatusCaller(caller.account_id, caller.consumer_id), diff --git a/src/adcp/reporting/receipts/__init__.py b/src/adcp/reporting/receipts/__init__.py index 551286cc7..a38e61fd2 100644 --- a/src/adcp/reporting/receipts/__init__.py +++ b/src/adcp/reporting/receipts/__init__.py @@ -1,6 +1,4 @@ -"""Durable seller receipt ingress. Optional PG driver is imported lazily.""" - -from typing import TYPE_CHECKING, Any +"""Durable seller receipt ingress. The PostgreSQL store is in ``adcp_reporting_storage``.""" from adcp.reporting.receipts.capture import ReportingReceiptBoundary from adcp.reporting.receipts.errors import ReceiptErrorCode, ReportingReceiptError @@ -8,12 +6,8 @@ from adcp.reporting.receipts.memory import InMemoryReportingReceiptStore from adcp.reporting.receipts.store import ReportingReceiptBatchStore, ReportingReceiptCaptureStore -if TYPE_CHECKING: - from adcp.reporting.receipts.pg import PgReportingReceiptStore - __all__ = [ "InMemoryReportingReceiptStore", - "PgReportingReceiptStore", "ReceiptAccountResolver", "ReceiptErrorCode", "ReportingReceiptBatchStore", @@ -22,11 +16,3 @@ "ReportingReceiptError", "ReportingReceiptHandler", ] - - -def __getattr__(name: str) -> Any: - if name == "PgReportingReceiptStore": - from adcp.reporting.receipts.pg import PgReportingReceiptStore - - return PgReportingReceiptStore - raise AttributeError(name) diff --git a/src/adcp/reporting/service.py b/src/adcp/reporting/service.py index ae2ba5ab6..ba973cc65 100644 --- a/src/adcp/reporting/service.py +++ b/src/adcp/reporting/service.py @@ -478,7 +478,13 @@ def __init__( production: ReportingProductionOptions | None = None, automated_recovery_window: timedelta = timedelta(hours=6), status_retention_days: int = 400, + staging: ReportingStagingStore | None = None, + seals: ReportingSealStore | None = None, + initialize_source_storage: Callable[[], Awaitable[None]] | None = None, ) -> None: + """``staging``/``seals`` are the default source stores adapters register with; + ``initialize_source_storage`` runs once at startup, after ``store.create_schema()``, + so a durable storage distribution can install its own schema.""" if production is not None and ( caller_resolver is not None or producer_factory is not ReportingProducer @@ -498,8 +504,8 @@ def __init__( self.store = store self._production: ReportingProductionSupport | None = None self._production_options = production - self._initialize_source_storage: Callable[[], Awaitable[None]] | None = None - self.sources = ReportingAdapterRegistry(clock=effective_clock) + self._initialize_source_storage = initialize_source_storage + self.sources = ReportingAdapterRegistry(clock=effective_clock, staging=staging, seals=seals) self._context_resolver = account_context self._caller_resolver = caller_resolver or self._default_caller self._consumer_status_enabled = consumer_status_enabled @@ -617,58 +623,14 @@ def memory( **kwargs, ) - @classmethod - def postgres( - cls, - *, - pool: Any, - account_context: ReportingContextResolver, - caller_resolver: ReportingCallerResolver | None = None, - clock: Callable[[], datetime] | None = None, - production: ReportingProductionOptions | None = None, - **kwargs: Any, - ) -> ReliableReportingService: - """Create durable ledger, staging and seal stores over a borrowed pool. - - ``production`` additionally composes the managed pipeline, receipts and - optional signed notifications when ``install`` freezes registered sources. - Startup installs all SDK-owned schemas. Explicit source-store overrides - remain the adopter's responsibility. No pool is opened or closed here. - """ - from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore - from adcp.reporting.production.pg import PgReportingProductionStore - - store = ( - PgReportingProductionStore( - pool=pool, clock=clock, notifications=production.notifications - ) - if production is not None - else PgReportingLedgerStore(pool=pool, clock=clock) - ) - service = cls( - store=store, - account_context=account_context, - caller_resolver=caller_resolver, - clock=clock, - production=production, - **kwargs, - ) - staging = PgReportingStagingStore(pool=pool) - service.sources = ReportingAdapterRegistry( - clock=service._clock, staging=staging, seals=PgReportingSealStore(pool=pool) - ) - service._initialize_source_storage = staging.create_schema - return service - def _compose_production(self) -> None: if self._production_options is None or self._production is not None: return - from adcp.reporting.production.memory import InMemoryReportingProductionStore - from adcp.reporting.production.pg import PgReportingProductionStore + from adcp.reporting.production.store import ReportingProductionStore - if not isinstance( - self.store, (InMemoryReportingProductionStore, PgReportingProductionStore) + if ( + not isinstance(self.store, ReportingProductionStore) + or not self.store.is_sdk_production_store() ): raise ReliableReportingConfigurationError( "production options require a memory or postgres production store" diff --git a/src/adcp/reporting/service_production.py b/src/adcp/reporting/service_production.py index a5b66be9b..2c1a049e4 100644 --- a/src/adcp/reporting/service_production.py +++ b/src/adcp/reporting/service_production.py @@ -30,15 +30,13 @@ ReportingProductionSupport, production_notification_workers, ) -from adcp.reporting.production.memory import InMemoryReportingProductionStore -from adcp.reporting.projection import InMemoryReportingStatusProjection, PgReportingStatusProjection +from adcp.reporting.production.store import ReportingProductionStore from adcp.reporting.receipts import ReceiptAccountResolver from adcp.types import ReportingDeliveryOffering if TYPE_CHECKING: from adcp.decisioning.registry import BuyerAgentRegistry from adcp.reporting.ledger import ReportingLedgerStore - from adcp.reporting.production.pg import PgReportingProductionStore from adcp.reporting.service import ReportingAdapterRegistry, ReportingContextResolver __all__ = ["ReportingServiceOffering", "ReportingProductionOptions"] @@ -108,7 +106,6 @@ def _capability_block( consumer_status_enabled: bool, ) -> dict[str, Any]: from adcp.reporting.production._declaration import declared_reporting_delivery - from adcp.reporting.production.pg import PgReportingProductionStore from adcp.reporting.service import _wire if set(sources.names) != {item.adapter for item in self.offerings}: @@ -116,7 +113,9 @@ def _capability_block( return declared_reporting_delivery( offerings=[_wire(item.offering) for item in self.offerings], destination=self.destination, - durable=type(store) is PgReportingProductionStore, + durable=isinstance(store, ReportingProductionStore) + and store.is_sdk_production_store() + and store.durable, escalation=escalation, consumer_status_enabled=consumer_status_enabled, automated_recovery_window=self.automated_recovery_window, @@ -127,17 +126,17 @@ def _capability_block( def _compose( self, *, - store: InMemoryReportingProductionStore | PgReportingProductionStore, + store: ReportingProductionStore, sources: ReportingAdapterRegistry, account_context: ReportingContextResolver, clock: Callable[[], datetime], escalation: ReportingDeliveryEscalation, consumer_status_enabled: bool, ) -> ReportingProductionSupport: - from adcp.reporting.production.pg import PgReportingProductionStore - if set(sources.names) != {item.adapter for item in self.offerings}: raise ValueError("every registered production adapter must have an offering") + if not isinstance(store, ReportingProductionStore) or not store.is_sdk_production_store(): + raise ValueError("production requires the factory's matching production store") source_registry = ReportingProductionSourceRegistry(account_context=account_context) profiles: dict[str, ReportingServiceOffering] = {} producers: dict[str, ReportingProducer] = {} @@ -173,23 +172,11 @@ def _compose( item.source_offering_id, ) ) - projection: InMemoryReportingStatusProjection | PgReportingStatusProjection - if type(store) is PgReportingProductionStore: - projection = PgReportingStatusProjection( - store, - consumer_status_enabled=consumer_status_enabled, - escalation=escalation, - revision_ownership=True, - ) - elif type(store) is InMemoryReportingProductionStore: - projection = InMemoryReportingStatusProjection( - store, - consumer_status_enabled=consumer_status_enabled, - escalation=escalation, - revision_ownership=True, - ) - else: - raise ValueError("production requires the factory's matching production store") + projection = store.status_projection( + consumer_status_enabled=consumer_status_enabled, + escalation=escalation, + revision_ownership=True, + ) workers: tuple[ReportingNotificationWorker, ...] = () if self.subscriptions is not None and self.cipher is not None and self.signing is not None: workers = production_notification_workers( diff --git a/src/adcp/reporting/submissions/__init__.py b/src/adcp/reporting/submissions/__init__.py index c4cff83d4..eb8d106c1 100644 --- a/src/adcp/reporting/submissions/__init__.py +++ b/src/adcp/reporting/submissions/__init__.py @@ -2,8 +2,9 @@ Public imports are additive and work without the optional PostgreSQL driver. The application supplies a trusted authorizer, a receipt client, and an explicit -intent store. Use PgReportingSubmissionIntentStore for restart durability; the -memory implementation is a volatile reference for tests. No checkpoint, seller +intent store. Use ``PgReportingSubmissionIntentStore`` from ``adcp_reporting_storage`` +for restart durability; the memory implementation is a volatile reference for tests. +No checkpoint, seller service, consumer-status, or client.reporting facade behavior is changed. Rollout: use a dedicated buyer schema/pool; explicitly call create_schema before @@ -25,7 +26,6 @@ ReportingSubmissionScope, prepare_reporting_receipt_submission, ) -from adcp.reporting.submissions.pg import PgReportingSubmissionIntentStore from adcp.reporting.submissions.store import ( InMemoryReportingSubmissionIntentStore, ReportingSubmissionIntentStore, @@ -38,7 +38,6 @@ __all__ = [ "InMemoryReportingSubmissionIntentStore", - "PgReportingSubmissionIntentStore", "ReportingReceiptFailureCode", "ReportingReceiptOutcome", "ReportingReceiptSubmission", diff --git a/tests/conformance/reporting/_buyer_submission_benchmark.py b/tests/conformance/reporting/_buyer_submission_benchmark.py index 644c86a12..b2f73490a 100644 --- a/tests/conformance/reporting/_buyer_submission_benchmark.py +++ b/tests/conformance/reporting/_buyer_submission_benchmark.py @@ -12,6 +12,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import argparse import asyncio import json @@ -20,9 +32,13 @@ import time from contextlib import asynccontextmanager +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.submissions import PgReportingSubmissionIntentStore +else: + from adcp_reporting_storage import PgReportingSubmissionIntentStore + from adcp.reporting.submissions import ( InMemoryReportingSubmissionIntentStore, - PgReportingSubmissionIntentStore, models, prepare_reporting_receipt_submission, submit_reporting_receipts, diff --git a/tests/conformance/reporting/_buyer_submission_installed.py b/tests/conformance/reporting/_buyer_submission_installed.py index dd6ebbb5a..f53118874 100644 --- a/tests/conformance/reporting/_buyer_submission_installed.py +++ b/tests/conformance/reporting/_buyer_submission_installed.py @@ -1,15 +1,35 @@ """Public Python 3.10 wheel checks, executed with -I outside the source tree.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib import importlib.util import json import sys -from importlib.resources import files from pathlib import Path +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + async def main(): settings = json.loads(sys.stdin.read()) assert sys.version_info[:2] == (3, 10) @@ -23,9 +43,13 @@ async def main(): origins[name] = str(origin) assert not any(Path(entry).resolve().is_relative_to(workspace) for entry in sys.path) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.submissions import PgReportingSubmissionIntentStore + else: + from adcp_reporting_storage import PgReportingSubmissionIntentStore + from adcp.reporting.submissions import ( InMemoryReportingSubmissionIntentStore, - PgReportingSubmissionIntentStore, ReportingSubmissionCode, ReportingSubmissionError, ReportingSubmissionScope, @@ -40,7 +64,7 @@ async def main(): from adcp.types.core import TaskResult, TaskStatus assert ReportingAdjustment.__name__ == "ReportingAdjustment" - sql = files("adcp.reporting.ledger").joinpath("reporting_buyer_submissions.sql").read_bytes() + sql = _asset_files(".ledger").joinpath("reporting_buyer_submissions.sql").read_bytes() assert hashlib.sha256(sql).hexdigest() == settings["sql_sha256"] if not settings["drivers"]: assert importlib.util.find_spec("psycopg") is None diff --git a/tests/conformance/reporting/_buyer_submission_process.py b/tests/conformance/reporting/_buyer_submission_process.py index b625f9075..d418f81b3 100644 --- a/tests/conformance/reporting/_buyer_submission_process.py +++ b/tests/conformance/reporting/_buyer_submission_process.py @@ -1,5 +1,17 @@ """Independent buyer + actual seller PG worker, paused for external SIGKILL.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -10,16 +22,16 @@ async def main(): settings = json.loads(await asyncio.to_thread(sys.stdin.readline)) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + from adcp.reporting.submissions import PgReportingSubmissionIntentStore + else: + from adcp_reporting_storage import PgReportingReceiptStore, PgReportingSubmissionIntentStore from psycopg import AsyncConnection from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ReportingDeliveryPrincipal - from adcp.reporting.receipts import PgReportingReceiptStore - from adcp.reporting.submissions import ( - PgReportingSubmissionIntentStore, - ReportingSubmissionScope, - submit_reporting_receipts, - ) + from adcp.reporting.submissions import ReportingSubmissionScope, submit_reporting_receipts from adcp.types import ( ReportingAdjustmentReceipt, ReportingReceipt, diff --git a/tests/conformance/reporting/_buyer_submission_support.py b/tests/conformance/reporting/_buyer_submission_support.py index 53fc3161a..a3b959bc2 100644 --- a/tests/conformance/reporting/_buyer_submission_support.py +++ b/tests/conformance/reporting/_buyer_submission_support.py @@ -2,15 +2,31 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + from collections.abc import AsyncIterator from dataclasses import dataclass from typing import Any import pytest +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.submissions import PgReportingSubmissionIntentStore +else: + from adcp_reporting_storage import PgReportingSubmissionIntentStore + from adcp.reporting.submissions import ( InMemoryReportingSubmissionIntentStore, - PgReportingSubmissionIntentStore, ReportingReceiptSubmissionClient, ReportingSubmissionIntentStore, ReportingSubmissionReceipt, diff --git a/tests/conformance/reporting/_durable_materializer_support.py b/tests/conformance/reporting/_durable_materializer_support.py index 733a5570b..6690e086d 100644 --- a/tests/conformance/reporting/_durable_materializer_support.py +++ b/tests/conformance/reporting/_durable_materializer_support.py @@ -1,5 +1,17 @@ """Shared deterministic vectors; PostgreSQL work always uses real database time.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib from contextlib import asynccontextmanager from copy import deepcopy @@ -157,7 +169,10 @@ async def durable_harness(backend, *, notifications=False): InMemoryReportingMaterializerStore(clock=clock, notifications=notifications), clock ) else: - from adcp.reporting.materializer.pg import PgReportingMaterializerStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.materializer.pg import PgReportingMaterializerStore + else: + from adcp_reporting_storage.materializer.pg import PgReportingMaterializerStore async with isolated_reporting_pool(autocommit=True) as pool: store = PgReportingMaterializerStore(pool=pool, notifications=notifications) diff --git a/tests/conformance/reporting/_empty_ledger_support.py b/tests/conformance/reporting/_empty_ledger_support.py index e0443da4a..0c1f2d93c 100644 --- a/tests/conformance/reporting/_empty_ledger_support.py +++ b/tests/conformance/reporting/_empty_ledger_support.py @@ -7,6 +7,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import os import secrets @@ -169,11 +181,11 @@ def install_legacy(connection: Any) -> None: def install_owned(connection: Any) -> None: - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") for name in OWNED_SQL: connection.execute(root.joinpath(name).read_text()) connection.execute( - files("adcp.reporting.production").joinpath("service_context.sql").read_text() + files("adcp_reporting_storage.production").joinpath("service_context.sql").read_text() ) @@ -182,7 +194,10 @@ def create_owned_schema(schema: str, **options: Any) -> None: pytest.importorskip("psycopg_pool") from psycopg_pool import AsyncConnectionPool - from adcp.reporting.ledger.pg import PgReportingLedgerStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger.pg import PgReportingLedgerStore + else: + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore async def create() -> None: async with AsyncConnectionPool( diff --git a/tests/conformance/reporting/_feed_installed_base.py b/tests/conformance/reporting/_feed_installed_base.py index 0d58155ed..358dd92eb 100644 --- a/tests/conformance/reporting/_feed_installed_base.py +++ b/tests/conformance/reporting/_feed_installed_base.py @@ -1,15 +1,35 @@ """Run outside the checkout on Python 3.10 without the optional PG drivers.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib import importlib.util import json import sys -from importlib.resources import files from pathlib import Path +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + async def main(settings): assert sys.version_info[:2] == (3, 10) assert importlib.util.find_spec("psycopg") is None @@ -21,12 +41,12 @@ async def main(settings): from adcp.server.mcp_tools import get_tools_for_handler from adcp.types import GetReportingStatusRequest, GetReportingStatusResponse - assert "adcp.reporting.feed.pg" not in sys.modules + assert "adcp_reporting_storage.feed.pg" not in sys.modules for name in feed.__all__: assert getattr(feed, name) is not None assert "psycopg" not in sys.modules and "psycopg_pool" not in sys.modules for relative, digest in settings["assets"].items(): - raw = files("adcp.reporting").joinpath(relative).read_bytes() + raw = _asset_files("").joinpath(relative).read_bytes() assert hashlib.sha256(raw).hexdigest() == digest prefix = "https://buyer.example.test/" caller = ReportingDeliveryPrincipal("installed-account", prefix + "a" * (2048 - len(prefix))) diff --git a/tests/conformance/reporting/_feed_process.py b/tests/conformance/reporting/_feed_process.py index b6c25c93a..622aa44e0 100644 --- a/tests/conformance/reporting/_feed_process.py +++ b/tests/conformance/reporting/_feed_process.py @@ -1,5 +1,17 @@ """Actual installed/source transport process; durable snapshot crash boundaries.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -7,16 +19,28 @@ import json import sys import traceback -from importlib.resources import files from pathlib import Path from types import SimpleNamespace +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + async def main(settings): + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + else: + from adcp_reporting_storage import PgReportingReceiptStore from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ReportingDeliveryPrincipal - from adcp.reporting.receipts import PgReportingReceiptStore, ReportingReceiptHandler + from adcp.reporting.receipts import ReportingReceiptHandler installed = settings.get("installed") origins = {} @@ -41,7 +65,10 @@ async def pause(point, **evidence): if receipt_only: store_type = PgReportingReceiptStore else: - from adcp.reporting.feed import PgReportingFeedStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + else: + from adcp_reporting_storage import PgReportingFeedStore class Store(PgReportingFeedStore): async def _capture_feed_on(self, connection, *args, **kwargs): @@ -68,8 +95,14 @@ async def _save_feed_snapshot_on(self, connection, stored): feed_operation_3 = await store.reporting_feed_ready() assert feed_operation_3 if settings.get("legacy_status_schema"): - from adcp.reporting.ledger import PgReportingReconciliationStore - from adcp.reporting.outbox import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import ( + PgReportingReconciliationStore, + PgStatusNotificationStore, + ) await PgStatusNotificationStore( PgReportingReconciliationStore(pool=pool, notifications=True) @@ -78,24 +111,18 @@ async def _save_feed_snapshot_on(self, connection, stored): "installed": True, "materializer_objects": len( json.loads( - files("adcp.reporting.materializer") - .joinpath("required_schema.json") - .read_text() + _asset_files(".materializer").joinpath("required_schema.json").read_text() ) ), "receipt_objects": len( json.loads( - files("adcp.reporting.receipts") - .joinpath("required_schema.json") - .read_text() + _asset_files(".receipts").joinpath("required_schema.json").read_text() ) ), } if not receipt_only: result["feed_objects"] = len( - json.loads( - files("adcp.reporting.feed").joinpath("required_schema.json").read_text() - ) + json.loads(_asset_files(".feed").joinpath("required_schema.json").read_text()) ) else: spec = importlib.util.spec_from_file_location("frozen_transport", settings["helper"]) diff --git a/tests/conformance/reporting/_feed_support.py b/tests/conformance/reporting/_feed_support.py index 26cb3a6a4..80aef5a2d 100644 --- a/tests/conformance/reporting/_feed_support.py +++ b/tests/conformance/reporting/_feed_support.py @@ -1,5 +1,17 @@ """Shared memory/PostgreSQL feed vectors and real mounted reporting reads.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib import json from contextlib import AsyncExitStack, asynccontextmanager @@ -37,7 +49,10 @@ async def feed_harness(backend, *, notifications=False): InMemoryReportingFeedStore(clock=clock, notifications=notifications), clock ) else: - from adcp.reporting.feed import PgReportingFeedStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + else: + from adcp_reporting_storage import PgReportingFeedStore async with isolated_reporting_pool(autocommit=True) as pool: # Autonomous writers use DB time. A historical application clock @@ -115,7 +130,10 @@ async def restart(h): if key not in {"_clock", "_lock"}: vars(store)[key] = deepcopy(value) else: - from adcp.reporting.feed import PgReportingFeedStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + else: + from adcp_reporting_storage import PgReportingFeedStore store = PgReportingFeedStore( pool=h.pool, clock=h.clock, notifications=h.store._notifications_enabled diff --git a/tests/conformance/reporting/_frozen_status_c.py b/tests/conformance/reporting/_frozen_status_c.py index 20e709f6e..2fed4049a 100644 --- a/tests/conformance/reporting/_frozen_status_c.py +++ b/tests/conformance/reporting/_frozen_status_c.py @@ -4,6 +4,18 @@ source. It is a test driver, not a production migration or recovery service. """ +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import json import sys @@ -24,13 +36,20 @@ async def main(): config = await read() root = Path(config["source"]).resolve() sys.path.insert(0, str(root / "src")) + if _storage_split(): # wheels before the storage split (#1510) + import adcp.reporting.outbox.status_pg as module + else: + import adcp_reporting_storage.outbox.status_pg as module + + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import PgReportingReconciliationStore, PgStatusNotificationStore from psycopg import AsyncConnection, Error from psycopg_pool import AsyncConnectionPool - import adcp.reporting.outbox.status_pg as module - from adcp.reporting.ledger import PgReportingReconciliationStore from adcp.reporting.ledger.notification_models import ReportingNotificationError - from adcp.reporting.outbox import PgStatusNotificationStore assert Path(module.__file__).is_relative_to(root) gate = None diff --git a/tests/conformance/reporting/_hardening_installed.py b/tests/conformance/reporting/_hardening_installed.py index c88169cef..6b13128da 100644 --- a/tests/conformance/reporting/_hardening_installed.py +++ b/tests/conformance/reporting/_hardening_installed.py @@ -1,5 +1,17 @@ """Run copied conformance fixtures against non-editable installed SDK bytes only.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import contextlib import hashlib import importlib @@ -7,12 +19,20 @@ import os import sys import time -from importlib.resources import files from pathlib import Path import pytest +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + class Results: def __init__(self): self.passed = self.failed = self.skipped = self.errors = self.deselected = 0 @@ -53,7 +73,7 @@ def main(settings): assert hashlib.sha256(path.read_bytes()).hexdigest() == expected origins[name] = str(path) for name, expected in settings["assets"].items(): - raw = files("adcp.reporting").joinpath(name).read_bytes() + raw = _asset_files("").joinpath(name).read_bytes() assert hashlib.sha256(raw).hexdigest() == expected if settings["driver_absent"]: assert importlib.util.find_spec("psycopg") is None diff --git a/tests/conformance/reporting/_hardening_packaging.py b/tests/conformance/reporting/_hardening_packaging.py index 80855d237..131bca685 100644 --- a/tests/conformance/reporting/_hardening_packaging.py +++ b/tests/conformance/reporting/_hardening_packaging.py @@ -1,5 +1,17 @@ """Installed hardening probes reuse fixtures, never current SDK source modules.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib import json import os @@ -8,14 +20,20 @@ import zipfile from pathlib import Path -from .test_reporting_notification_packaging import ROOT, run_step +from .test_reporting_notification_packaging import ( + ROOT, + STORAGE_SRC, + is_current_build, + run_step, + storage_distribution, +) MODULES = ( "adcp.reporting.outbox.support", - "adcp.reporting.outbox._schema", - "adcp.reporting.outbox.status_schema", + "adcp_reporting_storage.outbox._schema", + "adcp_reporting_storage.outbox.status_schema", "adcp.reporting.receipts.handler", - "adcp.reporting.receipts.pg", + "adcp_reporting_storage.receipts.pg", "adcp.reporting.receipts._diagnostics", ) ASSETS = ( @@ -56,26 +74,56 @@ def installed_hardening(root, python, wheel, *, label, parent=False, driver_abse cwd=root, timeout=180, ) - with zipfile.ZipFile(wheel) as archive: - modules = { - name: hashlib.sha256(archive.read(name.replace(".", "/") + ".py")).hexdigest() - for name in MODULES - if name.replace(".", "/") + ".py" in archive.namelist() - } - assets = { - name: hashlib.sha256(archive.read("adcp/reporting/" + name)).hexdigest() - for name in ASSETS - } - for path in [ - *(name.replace(".", "/") + ".py" for name in modules), - *("adcp/reporting/" + name for name in assets), - ]: - expected = ( - subprocess.check_output(["git", "show", f"{B23}:src/{path}"], cwd=ROOT) - if parent - else (ROOT / "src" / path).read_bytes() - ) - assert archive.read(path) == expected + # A current build is two wheels: the stores, DDL and catalog manifests are in + # the storage wheel. A historical wheel (the B23 parent) carries them itself. + current = is_current_build(wheel) + storage = zipfile.ZipFile(storage_distribution(wheel)) if current else None + + def located(name): + """(archive, member path, expected bytes) for a module or asset name.""" + if name in ASSETS: + if current: + member, source = "adcp_reporting_storage/" + name, STORAGE_SRC / name + else: + member, source = "adcp/reporting/" + name, ROOT / "src/adcp/reporting" / name + elif name.startswith("adcp_reporting_storage."): + member = name.replace(".", "/") + ".py" + source = STORAGE_SRC.parent / member + if not current: + member = member.replace("adcp_reporting_storage/", "adcp/reporting/", 1) + else: + member = name.replace(".", "/") + ".py" + source = ROOT / "src" / member + archive = ( + storage if current and member.startswith("adcp_reporting_storage/") else wheel_archive + ) + + def expected(): + # Resolved only for members the archive carries: a module newer than + # the parent has no bytes at the parent commit. + if parent: + return subprocess.check_output(["git", "show", f"{B23}:src/{member}"], cwd=ROOT) + return source.read_bytes() + + return archive, member, expected + + with zipfile.ZipFile(wheel) as wheel_archive: + modules, assets = {}, {} + for name in MODULES: + archive, member, expected = located(name) + if member not in archive.namelist(): + continue + raw = archive.read(member) + assert raw == expected(), member + key = name if current else name.replace("adcp_reporting_storage.", "adcp.reporting.", 1) + modules[key] = hashlib.sha256(raw).hexdigest() + for name in ASSETS: + archive, member, expected = located(name) + raw = archive.read(member) + assert raw == expected(), member + assets[name] = hashlib.sha256(raw).hexdigest() + if storage is not None: + storage.close() settings = { "workspace": str(ROOT), "fixtures": str(fixture_root), diff --git a/tests/conformance/reporting/_inline_storage_worker.py b/tests/conformance/reporting/_inline_storage_worker.py index 227c2347e..638bc66f4 100644 --- a/tests/conformance/reporting/_inline_storage_worker.py +++ b/tests/conformance/reporting/_inline_storage_worker.py @@ -2,6 +2,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import argparse import asyncio import base64 @@ -10,11 +22,14 @@ from datetime import datetime, timezone from pathlib import Path +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore +else: + from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore from psycopg_pool import AsyncConnectionPool from adcp.reporting.fixtures import redacted_capabilities, redacted_snapshot_request from adcp.reporting.inline_source import InlineReportingSource -from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore async def run(mode: str, schema: str, output: Path, counter: Path, value: int) -> None: diff --git a/tests/conformance/reporting/_late_account_server.py b/tests/conformance/reporting/_late_account_server.py index dce87dce0..46acbb24c 100644 --- a/tests/conformance/reporting/_late_account_server.py +++ b/tests/conformance/reporting/_late_account_server.py @@ -1,5 +1,17 @@ """Public production support starts empty; all enrollment arrives over real HTTP.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import argparse import importlib.metadata import json @@ -7,6 +19,11 @@ from datetime import datetime, timezone from pathlib import Path +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.production import PgReportingProductionStore + from adcp.reporting.projection import PgReportingStatusProjection +else: + from adcp_reporting_storage import PgReportingProductionStore, PgReportingStatusProjection from psycopg_pool import AsyncConnectionPool import adcp @@ -25,13 +42,11 @@ ReportingWriterCapability, ) from adcp.reporting.production import ( - PgReportingProductionStore, ReportingConfigurationAdmission, ReportingProductionConfigurationTask, ReportingProductionOffering, ReportingProductionSupport, ) -from adcp.reporting.projection import PgReportingStatusProjection from adcp.reporting.receipts import ReportingReceiptError from adcp.reporting.source import parse_verified_source_batch_manifest_v1 from adcp.server import serve diff --git a/tests/conformance/reporting/_legacy_row_first_lease.py b/tests/conformance/reporting/_legacy_row_first_lease.py index 82f119712..0327b8555 100644 --- a/tests/conformance/reporting/_legacy_row_first_lease.py +++ b/tests/conformance/reporting/_legacy_row_first_lease.py @@ -5,9 +5,25 @@ behavior. Fixing the live base method must not silently disable this control. """ +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + from datetime import datetime, timedelta -from adcp.reporting.ledger.pg import _utc +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger.pg import _utc +else: + from adcp_reporting_storage.ledger.pg import _utc + from adcp.reporting.ledger.store import LeasedConfiguration diff --git a/tests/conformance/reporting/_materializer_frozen.py b/tests/conformance/reporting/_materializer_frozen.py index 4a61553f6..1146d76a7 100644 --- a/tests/conformance/reporting/_materializer_frozen.py +++ b/tests/conformance/reporting/_materializer_frozen.py @@ -1,5 +1,17 @@ """Copied out of the checkout and run with -I in an installed frozen wheel.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -14,10 +26,13 @@ async def main(settings): + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingLedgerStore + else: + from adcp_reporting_storage import PgReportingLedgerStore from psycopg_pool import AsyncConnectionPool import adcp.reporting.ledger as ledger_module - from adcp.reporting.ledger import PgReportingLedgerStore origins = {} for name, expected in settings["modules"].items(): @@ -51,13 +66,19 @@ async def main(settings): if settings["action"] == "install": await store.create_schema() if settings["artifact"] in {"c", "b1"}: - from adcp.reporting.outbox import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import PgStatusNotificationStore await PgStatusNotificationStore( store_type(pool=pool, notifications=True) ).create_schema() if settings["artifact"] in {"a", "b", "c", "b1"}: - from adcp.reporting.outbox._schema import validate_schema + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox._schema import validate_schema + else: + from adcp_reporting_storage.outbox._schema import validate_schema async with pool.connection() as connection: await validate_schema(connection) @@ -75,8 +96,12 @@ async def main(settings): notifications_ready = None if settings["artifact"] in {"a", "b", "c", "b1"}: + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox._schema import validate_schema + else: + from adcp_reporting_storage.outbox._schema import validate_schema + from adcp.reporting.ledger.notification_models import ReportingNotificationError - from adcp.reporting.outbox._schema import validate_schema async with pool.connection() as connection: try: @@ -134,7 +159,10 @@ async def main(settings): status = None projector_turns = 0 if settings["artifact"] in {"c", "b1"} and settings["action"] != "baseline": - from adcp.reporting.outbox import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import PgStatusNotificationStore store = store_type(pool=pool, notifications=True) status = PgStatusNotificationStore(store) @@ -160,12 +188,13 @@ async def main(settings): workers = {} event_identities = {} if settings["artifact"] in {"a", "b", "c", "b1"} and settings["action"] != "baseline": + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgReportingOutbox + else: + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import decode_event - from adcp.reporting.outbox import ( - PgReportingOutbox, - ReportingEnvelopeCipher, - ReportingNotificationWorker, - ) + from adcp.reporting.outbox import ReportingEnvelopeCipher, ReportingNotificationWorker class EmptySubscriptions: async def list_active(self, **kwargs): @@ -177,7 +206,10 @@ async def get_active(self, **kwargs): outboxes = {"ordinary": PgReportingOutbox(pool=pool)} if settings["artifact"] in {"c", "b1"}: - from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox + else: + from adcp_reporting_storage.outbox.status_pg import PgReportingStatusOutbox outboxes["status"] = PgReportingStatusOutbox(pool=pool) for name, outbox in outboxes.items(): diff --git a/tests/conformance/reporting/_materializer_installed.py b/tests/conformance/reporting/_materializer_installed.py index 22ec004ac..e8e96cb04 100644 --- a/tests/conformance/reporting/_materializer_installed.py +++ b/tests/conformance/reporting/_materializer_installed.py @@ -1,5 +1,17 @@ """Copied outside the checkout and executed with Python 3.10 -I against a wheel.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -14,6 +26,15 @@ from pydantic import TypeAdapter +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + async def main(): config = json.load(sys.stdin) assert sys.version_info[:2] == (3, 10) @@ -67,14 +88,12 @@ async def main(): for name in config["assets"] } assert actual == config["assets"] - assert ( - files("adcp.reporting.ledger").joinpath("reporting_status_selector_version.sql").is_file() - ) - assert files("adcp.reporting.outbox").joinpath("required_status_selector_schema.json").is_file() - assert files("adcp.reporting.ledger").joinpath("reporting_materializer.sql").is_file() - assert files("adcp.reporting.materializer").joinpath("required_schema.json").is_file() - assert files("adcp.reporting.ledger").joinpath("reporting_receipt_ingestion.sql").is_file() - assert files("adcp.reporting.receipts").joinpath("required_schema.json").is_file() + assert _asset_files(".ledger").joinpath("reporting_status_selector_version.sql").is_file() + assert _asset_files(".outbox").joinpath("required_status_selector_schema.json").is_file() + assert _asset_files(".ledger").joinpath("reporting_materializer.sql").is_file() + assert _asset_files(".materializer").joinpath("required_schema.json").is_file() + assert _asset_files(".ledger").joinpath("reporting_receipt_ingestion.sql").is_file() + assert _asset_files(".receipts").joinpath("required_schema.json").is_file() start = datetime(2026, 9, 1, tzinfo=timezone.utc) schedule = ReportingScheduleSpec("PT1H", "PT1H", period_anchor=start) period = derive_period(schedule, account_timezone="UTC", ordinal=0) diff --git a/tests/conformance/reporting/_materializer_process.py b/tests/conformance/reporting/_materializer_process.py index 6574bf24b..ae45e2d62 100644 --- a/tests/conformance/reporting/_materializer_process.py +++ b/tests/conformance/reporting/_materializer_process.py @@ -1,5 +1,17 @@ """Real worker process and conditional file destination, solely for crash tests.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -21,13 +33,16 @@ async def read(): async def main(): settings = await read() + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.materializer import PgReportingMaterializerStore + else: + from adcp_reporting_storage import PgReportingMaterializerStore from psycopg import AsyncConnection from psycopg_pool import AsyncConnectionPool from pydantic import TypeAdapter from adcp.reporting.ledger._delivery_state import decode_record from adcp.reporting.materializer import ( - PgReportingMaterializerStore, ReferenceReportingDestinationWriter, ReferenceReportingResolver, ReportingDestinationIO, diff --git a/tests/conformance/reporting/_ownership_archive_restart.py b/tests/conformance/reporting/_ownership_archive_restart.py index 2aeddb1a9..629ac5f94 100644 --- a/tests/conformance/reporting/_ownership_archive_restart.py +++ b/tests/conformance/reporting/_ownership_archive_restart.py @@ -1,5 +1,17 @@ """Installed floor process proves an archived lease cannot become runnable.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib.util @@ -9,12 +21,15 @@ async def main(settings): + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + else: + from adcp_reporting_storage import PgReportingFeedStore from psycopg import sql from psycopg_pool import AsyncConnectionPool from pydantic import TypeAdapter import adcp - from adcp.reporting.feed import PgReportingFeedStore from adcp.reporting.materializer import ReportingVerificationKey from adcp.reporting.materializer.work import ReportingMaterializerLease diff --git a/tests/conformance/reporting/_ownership_legacy_seed.py b/tests/conformance/reporting/_ownership_legacy_seed.py index 972cbd060..65e2c2a4c 100644 --- a/tests/conformance/reporting/_ownership_legacy_seed.py +++ b/tests/conformance/reporting/_ownership_legacy_seed.py @@ -4,6 +4,18 @@ operator mapping from the test's authoritative registry, never a buyer request. """ +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -58,16 +70,24 @@ async def main(settings): ) as pool: if settings["action"] == "seed": sys.path.insert(0, settings["fixtures"]) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import ( + PgReportingFeedStore, + PgReportingReconciliationStore, + PgStatusNotificationStore, + ) + from adcp._version import resolve_adcp_version - from adcp.reporting.feed import PgReportingFeedStore from adcp.reporting.ledger import ( - PgReportingReconciliationStore, ReportingAdjustmentRecord, ReportingControlTotalRecord, ReportingRevisionReceiptRecord, ) from adcp.reporting.ledger.delivery import adjustment_to_wire, receipt_to_wire - from adcp.reporting.outbox import PgStatusNotificationStore from tests.conformance.reporting._durable_materializer_support import durable_case from tests.conformance.reporting._generation_support import END @@ -240,17 +260,31 @@ async def main(settings): "pending": pending, } else: + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + from adcp.reporting.ledger import PgReportingLedgerStore + else: + from adcp_reporting_storage import PgReportingFeedStore, PgReportingLedgerStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.migration import ( + ReportingOwnershipBackfill, + ReportingOwnershipMigrationError, + backfill_legacy_reporting, + legacy_generation_digest, + migrate_legacy_reporting, + ) + else: + from adcp_reporting_storage.migration import ( + ReportingOwnershipBackfill, + ReportingOwnershipMigrationError, + backfill_legacy_reporting, + legacy_generation_digest, + migrate_legacy_reporting, + ) from psycopg import sql - from adcp.reporting.feed import PgReportingFeedStore, ReportingFeedError - from adcp.reporting.ledger import PgReportingLedgerStore, ReportingStatusCaller - from adcp.reporting.migration import ( - ReportingOwnershipBackfill, - ReportingOwnershipMigrationError, - backfill_legacy_reporting, - legacy_generation_digest, - migrate_legacy_reporting, - ) + from adcp.reporting.feed import ReportingFeedError + from adcp.reporting.ledger import ReportingStatusCaller store = PgReportingFeedStore(pool=pool, notifications=settings["notifications"]) legacy = settings["legacy"] diff --git a/tests/conformance/reporting/_production_delivery_process.py b/tests/conformance/reporting/_production_delivery_process.py index 655238e12..5caab821c 100644 --- a/tests/conformance/reporting/_production_delivery_process.py +++ b/tests/conformance/reporting/_production_delivery_process.py @@ -1,5 +1,17 @@ """Real SIGKILL after receiver acceptance; cold retry uses the durable deadline.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import json @@ -11,6 +23,15 @@ async def main(settings): import pytest + + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.production.pg import PgReportingProductionStore + else: + from adcp_reporting_storage.production.pg import PgReportingProductionStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.projection.pg import PgReportingStatusProjection + else: + from adcp_reporting_storage.projection.pg import PgReportingStatusProjection from psycopg_pool import AsyncConnectionPool import adcp.reporting.production.delivery_window as window_module @@ -19,8 +40,6 @@ async def main(settings): ReportingProductionSigning, production_notification_workers, ) - from adcp.reporting.production.pg import PgReportingProductionStore - from adcp.reporting.projection.pg import PgReportingStatusProjection from ._reliable_support import ( Barrier, diff --git a/tests/conformance/reporting/_production_installed.py b/tests/conformance/reporting/_production_installed.py index 7e6bbc3e3..e6c8c5293 100644 --- a/tests/conformance/reporting/_production_installed.py +++ b/tests/conformance/reporting/_production_installed.py @@ -1,5 +1,17 @@ """Floor-runtime conformance with complete installed module/schema provenance.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import contextlib import hashlib import importlib @@ -16,6 +28,15 @@ import pytest +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + def assert_installed_schema_keys(schemas, pinned): assert set(schemas) == {pinned} @@ -54,10 +75,7 @@ def enter_phase(name): origins[name] = str(path) enter_phase("installed_assets") for name, expected in settings["assets"].items(): - assert ( - hashlib.sha256(files("adcp.reporting").joinpath(name).read_bytes()).hexdigest() - == expected - ) + assert hashlib.sha256(_asset_files("").joinpath(name).read_bytes()).hexdigest() == expected from adcp.validation import schema_loader enter_phase("installed_current_schemas") diff --git a/tests/conformance/reporting/_production_installed_process.py b/tests/conformance/reporting/_production_installed_process.py index 12ad004e0..65f2a1ec6 100644 --- a/tests/conformance/reporting/_production_installed_process.py +++ b/tests/conformance/reporting/_production_installed_process.py @@ -1,16 +1,36 @@ """Installed activation, SIGKILL, and immutable legacy/new representation walks.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib import json import sys from dataclasses import asdict -from importlib.resources import files from pathlib import Path from types import SimpleNamespace +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + async def main(settings): root = Path(settings["fixtures"]) assert not (root / "src").exists() and not (root / "adcp").exists() @@ -23,10 +43,7 @@ async def main(settings): assert hashlib.sha256(path.read_bytes()).hexdigest() == digest origins[name] = str(path) for name, digest in settings["assets"].items(): - assert ( - hashlib.sha256(files("adcp.reporting").joinpath(name).read_bytes()).hexdigest() - == digest - ) + assert hashlib.sha256(_asset_files("").joinpath(name).read_bytes()).hexdigest() == digest from psycopg_pool import AsyncConnectionPool from pydantic import TypeAdapter diff --git a/tests/conformance/reporting/_production_legacy_process.py b/tests/conformance/reporting/_production_legacy_process.py index 2328e2ebb..e89683fd4 100644 --- a/tests/conformance/reporting/_production_legacy_process.py +++ b/tests/conformance/reporting/_production_legacy_process.py @@ -1,5 +1,17 @@ """Executed by the real historical binary: incompatible C writers fail closed.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import json import sys @@ -11,15 +23,28 @@ async def main(settings): import hashlib + if _storage_split(): # wheels before the storage split (#1510) + import adcp.reporting.materializer.pg as materializer + else: + import adcp_reporting_storage.materializer.pg as materializer + if _storage_split(): # wheels before the storage split (#1510) + import adcp.reporting.outbox.status_pg as implementation + else: + import adcp_reporting_storage.outbox.status_pg as implementation + + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import PgReportingFeedStore + else: + from adcp_reporting_storage import PgReportingFeedStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox.status_pg import PgStatusNotificationStore + else: + from adcp_reporting_storage.outbox.status_pg import PgStatusNotificationStore from psycopg_pool import AsyncConnectionPool from pydantic import TypeAdapter - import adcp.reporting.materializer.pg as materializer - import adcp.reporting.outbox.status_pg as implementation - from adcp.reporting.feed import PgReportingFeedStore from adcp.reporting.materializer import ReportingVerificationKey from adcp.reporting.materializer.work import ReportingMaterializerLease - from adcp.reporting.outbox.status_pg import PgStatusNotificationStore path = Path(implementation.__file__).resolve() assert path.is_relative_to(Path(sys.prefix)) diff --git a/tests/conformance/reporting/_production_packaging.py b/tests/conformance/reporting/_production_packaging.py index a9991b106..0f246e56c 100644 --- a/tests/conformance/reporting/_production_packaging.py +++ b/tests/conformance/reporting/_production_packaging.py @@ -1,5 +1,17 @@ """Build and inspect actual production distributions; fixtures never alias SDK source.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib import json import os @@ -9,7 +21,13 @@ import zipfile from pathlib import Path -from .test_reporting_notification_packaging import ROOT, run_step +from .test_reporting_notification_packaging import ( + ROOT, + STORAGE_SRC, + run_step, + storage_distribution, + storage_source, +) ASSETS = ( "ledger/reporting_materializer.sql", @@ -107,32 +125,51 @@ def production_modules(): "types/domains/bundled/protocol/get_adcp_capabilities_response.py", ) ] - return { - "adcp." - + str(p.relative_to(ROOT / "src/adcp")) - .removesuffix(".py") - .replace("/", ".") - .removesuffix(".__init__"): hashlib.sha256(p.read_bytes()) - .hexdigest() - for p in paths - } + modules = {} + for p in paths: + relative = p.relative_to(ROOT / "src/adcp") + if not p.exists() and str(relative).startswith("reporting/"): + # The PostgreSQL stores moved to the storage distribution (#1510). + p = STORAGE_SRC / relative.relative_to("reporting") + name = "adcp_reporting_storage." + str(relative.relative_to("reporting")) + else: + name = "adcp." + str(relative) + modules[name.removesuffix(".py").replace("/", ".").removesuffix(".__init__")] = ( + hashlib.sha256(p.read_bytes()).hexdigest() + ) + return modules def inspect_distribution(wheel, source): + """Check both wheels of a current build; the stores and DDL are in the storage wheel.""" modules = production_modules() - assets = {name: (ROOT / "src/adcp/reporting" / name).read_bytes() for name in ASSETS} - with zipfile.ZipFile(wheel) as archive, tarfile.open(source) as sdist: + assets = {name: (STORAGE_SRC / name).read_bytes() for name in ASSETS} + with ( + zipfile.ZipFile(wheel) as archive, + tarfile.open(source) as sdist, + zipfile.ZipFile(storage_distribution(wheel)) as storage_archive, + tarfile.open(storage_source(wheel)) as storage_sdist, + ): prefix = sdist.getnames()[0].split("/")[0] + storage_prefix = storage_sdist.getnames()[0].split("/")[0] for name, digest in modules.items(): member = name.replace(".", "/") + ".py" - if member not in archive.namelist(): + zipped, tarred, tar_prefix = archive, sdist, prefix + if name.startswith("adcp_reporting_storage."): + zipped, tarred, tar_prefix = storage_archive, storage_sdist, storage_prefix + if member not in zipped.namelist(): member = name.replace(".", "/") + "/__init__.py" - raw = archive.read(member) + raw = zipped.read(member) assert hashlib.sha256(raw).hexdigest() == digest - assert sdist.extractfile(f"{prefix}/src/{member}").read() == raw + assert tarred.extractfile(f"{tar_prefix}/src/{member}").read() == raw for name, raw in assets.items(): - assert archive.read("adcp/reporting/" + name) == raw - assert sdist.extractfile(f"{prefix}/src/adcp/reporting/{name}").read() == raw + assert storage_archive.read("adcp_reporting_storage/" + name) == raw + assert ( + storage_sdist.extractfile( + f"{storage_prefix}/src/adcp_reporting_storage/{name}" + ).read() + == raw + ) for version in RETAINED_SCHEMA_VERSIONS: for path in sorted((ROOT / "src/adcp/_compliance" / version).rglob("*")): if path.is_file(): diff --git a/tests/conformance/reporting/_production_support.py b/tests/conformance/reporting/_production_support.py index d0db383bf..28bbae998 100644 --- a/tests/conformance/reporting/_production_support.py +++ b/tests/conformance/reporting/_production_support.py @@ -4,6 +4,18 @@ eligibility flag. Its immutable rows and grants survive a new provider process. """ +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib import inspect import json @@ -393,7 +405,10 @@ async def production_harness( elif backend == "memory": store = InMemoryReportingProductionStore(clock=clock, notifications=notifications) else: - from adcp.reporting.production.pg import PgReportingProductionStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.production.pg import PgReportingProductionStore + else: + from adcp_reporting_storage.production.pg import PgReportingProductionStore if pool is None: pool = await stack.enter_async_context(isolated_reporting_pool(autocommit=True)) @@ -534,7 +549,10 @@ async def production_harness( consumer_status_enabled=feedback, ) else: - from adcp.reporting.projection.pg import PgReportingStatusProjection + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.projection.pg import PgReportingStatusProjection + else: + from adcp_reporting_storage.projection.pg import PgReportingStatusProjection projection = PgReportingStatusProjection( store, diff --git a/tests/conformance/reporting/_projection_support.py b/tests/conformance/reporting/_projection_support.py index 5feeb53a5..aae46572c 100644 --- a/tests/conformance/reporting/_projection_support.py +++ b/tests/conformance/reporting/_projection_support.py @@ -1,5 +1,17 @@ """B2.4 shared execution: actual stores, no forged readiness certificate.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + from contextlib import asynccontextmanager import pytest @@ -25,10 +37,16 @@ async def projection_harness(backend, *, notifications=False, feedback=False, ow ) yield h else: - from adcp.reporting.projection.pg import ( - PgReportingProjectionStore, - PgReportingStatusProjection, - ) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.projection.pg import ( + PgReportingProjectionStore, + PgReportingStatusProjection, + ) + else: + from adcp_reporting_storage.projection.pg import ( + PgReportingProjectionStore, + PgReportingStatusProjection, + ) async with isolated_reporting_pool(autocommit=True) as pool: store = PgReportingProjectionStore(pool=pool, notifications=notifications) diff --git a/tests/conformance/reporting/_provisional_catalog.py b/tests/conformance/reporting/_provisional_catalog.py index 7be0062d9..801def976 100644 --- a/tests/conformance/reporting/_provisional_catalog.py +++ b/tests/conformance/reporting/_provisional_catalog.py @@ -1,5 +1,22 @@ """Exact additive observation catalog; historical manifests remain unchanged.""" -from adcp.reporting.outbox._schema import PROVISIONAL_REQUIRED_OBJECTS as PROVISIONAL_OBJECTS +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox._schema import PROVISIONAL_REQUIRED_OBJECTS as PROVISIONAL_OBJECTS +else: + from adcp_reporting_storage.outbox._schema import ( + PROVISIONAL_REQUIRED_OBJECTS as PROVISIONAL_OBJECTS, + ) assert len(PROVISIONAL_OBJECTS) == 27 diff --git a/tests/conformance/reporting/_rc6_continuation_process.py b/tests/conformance/reporting/_rc6_continuation_process.py index 387be6231..20a57e054 100644 --- a/tests/conformance/reporting/_rc6_continuation_process.py +++ b/tests/conformance/reporting/_rc6_continuation_process.py @@ -2,6 +2,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -55,13 +67,25 @@ async def fresh_read_after_legacy_refusals(mount, account_id, positions): async def main(settings): origins = verify_installation(settings) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.feed import pg as feed_pg + else: + from adcp_reporting_storage.feed import pg as feed_pg + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.projection.pg import ( + PgReportingProjectionStore, + PgReportingStatusProjection, + ) + else: + from adcp_reporting_storage.projection.pg import ( + PgReportingProjectionStore, + PgReportingStatusProjection, + ) from psycopg_pool import AsyncConnectionPool from adcp.reporting.canonical_json import canonical_json_utf8_v1 - from adcp.reporting.feed import pg as feed_pg from adcp.reporting.ledger import ProducerOfferings, ReportingProducer from adcp.reporting.ledger.delivery_models import ReportingDeliveryPrincipal - from adcp.reporting.projection.pg import PgReportingProjectionStore, PgReportingStatusProjection from adcp.types import GetReportingStatusRequest from adcp.validation.schema_loader import get_named_validator, get_validator from tests.conformance.reporting._feed_support import MountedFeed diff --git a/tests/conformance/reporting/_receipt_frozen.py b/tests/conformance/reporting/_receipt_frozen.py index 1a99dccb0..4446d35bd 100644 --- a/tests/conformance/reporting/_receipt_frozen.py +++ b/tests/conformance/reporting/_receipt_frozen.py @@ -1,5 +1,26 @@ """Installed historical reader/writer probe, copied out and run with python -I.""" +import importlib.util as _importlib_util + + +def _asset_files(package): + """Packaged reporting assets: the storage distribution's, or adcp's before the split.""" + from importlib.resources import files as _files + + if _storage_split(): + return _files("adcp.reporting" + package) + return _files("adcp_reporting_storage" + package) + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -9,7 +30,6 @@ import traceback from dataclasses import replace from datetime import datetime, timezone -from importlib.resources import files from pathlib import Path from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller @@ -37,11 +57,17 @@ async def main(settings): settings["conninfo"], kwargs=settings["kwargs"], min_size=1, max_size=2, open=False ) as pool: if settings["artifact"] == "b22": - from adcp.reporting.receipts import PgReportingReceiptStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + else: + from adcp_reporting_storage import PgReportingReceiptStore store_type = PgReportingReceiptStore elif settings["artifact"] == "b21": - from adcp.reporting.materializer import PgReportingMaterializerStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.materializer import PgReportingMaterializerStore + else: + from adcp_reporting_storage import PgReportingMaterializerStore store_type = PgReportingMaterializerStore else: @@ -61,7 +87,10 @@ async def main(settings): # Use this actual parent binary's reviewed Core composition when # preparing the rolling baseline; the materializer migration does # not implicitly install or activate C's projector. - from adcp.reporting.outbox import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import PgStatusNotificationStore await PgStatusNotificationStore( ledger.PgReportingReconciliationStore(pool=pool, notifications=True) @@ -69,7 +98,7 @@ async def main(settings): feed_operation_1 = await store.materializer_ready() assert feed_operation_1 manifest = json.loads( - files("adcp.reporting.materializer").joinpath("required_schema.json").read_text() + _asset_files(".materializer").joinpath("required_schema.json").read_text() ) assert len(manifest) == 187 return {"installed": True, "manifest_objects": len(manifest), "origins": origins} @@ -91,7 +120,10 @@ async def main(settings): assert len(revisions) == 1 and revisions[0].finality == "official" status, projector_turns = None, 0 if settings["notifications"] and settings["artifact"] in {"c", "b1", "b21", "b22"}: - from adcp.reporting.outbox import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgStatusNotificationStore + else: + from adcp_reporting_storage import PgStatusNotificationStore status = PgStatusNotificationStore( ledger.PgReportingReconciliationStore(pool=pool, notifications=True) @@ -166,8 +198,12 @@ async def project_ordinary_status(): await project_ordinary_status() readiness = None if settings["artifact"] in {"a", "b", "c", "b1", "b21", "b22"}: + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox._schema import validate_schema + else: + from adcp_reporting_storage.outbox._schema import validate_schema + from adcp.reporting.ledger.notification_models import ReportingNotificationError - from adcp.reporting.outbox._schema import validate_schema async with pool.connection() as connection: try: @@ -197,20 +233,19 @@ async def project_ordinary_status(): assert epoch == (0, "acked") materializer = len( json.loads( - files("adcp.reporting.materializer") - .joinpath("required_schema.json") - .read_text() + _asset_files(".materializer").joinpath("required_schema.json").read_text() ) ) assert materializer == 187 workers = {} if settings["notifications"]: + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import PgReportingOutbox + else: + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import decode_event - from adcp.reporting.outbox import ( - PgReportingOutbox, - ReportingEnvelopeCipher, - ReportingNotificationWorker, - ) + from adcp.reporting.outbox import ReportingEnvelopeCipher, ReportingNotificationWorker class EmptySubscriptions: async def list_active(self, **kwargs): diff --git a/tests/conformance/reporting/_receipt_installed.py b/tests/conformance/reporting/_receipt_installed.py index ef9381a26..79bbcbc2a 100644 --- a/tests/conformance/reporting/_receipt_installed.py +++ b/tests/conformance/reporting/_receipt_installed.py @@ -1,5 +1,17 @@ """Installed Python 3.10 SQL and authenticated HTTP replay, run outside the checkout.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -12,9 +24,13 @@ async def main(settings): + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + else: + from adcp_reporting_storage import PgReportingReceiptStore from psycopg_pool import AsyncConnectionPool - from adcp.reporting.receipts import PgReportingReceiptStore, ReportingReceiptError + from adcp.reporting.receipts import ReportingReceiptError installed = settings["installed"] workspace = Path(installed["workspace"]).resolve() diff --git a/tests/conformance/reporting/_receipt_process.py b/tests/conformance/reporting/_receipt_process.py index 7a336fe11..7e2604bb8 100644 --- a/tests/conformance/reporting/_receipt_process.py +++ b/tests/conformance/reporting/_receipt_process.py @@ -1,5 +1,17 @@ """Real receipt worker. Pause while holding an ordinal transaction, then SIGKILL.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import importlib @@ -10,11 +22,14 @@ async def main(): settings = json.loads(await asyncio.to_thread(sys.stdin.readline)) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + else: + from adcp_reporting_storage import PgReportingReceiptStore from psycopg import AsyncConnection from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ReportingDeliveryPrincipal - from adcp.reporting.receipts import PgReportingReceiptStore origins = {} if settings.get("installed"): diff --git a/tests/conformance/reporting/_receipt_support.py b/tests/conformance/reporting/_receipt_support.py index e7ac80dce..d31373c7e 100644 --- a/tests/conformance/reporting/_receipt_support.py +++ b/tests/conformance/reporting/_receipt_support.py @@ -1,5 +1,17 @@ """Shared receipt-ingress vectors; production PG receipt time is database time.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + from contextlib import asynccontextmanager from dataclasses import dataclass, replace from datetime import timedelta @@ -30,7 +42,10 @@ async def receipt_harness(backend, *, notifications=False, pool=None): InMemoryReportingReceiptStore(clock=clock, notifications=notifications), clock ) return - from adcp.reporting.receipts import PgReportingReceiptStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import PgReportingReceiptStore + else: + from adcp_reporting_storage import PgReportingReceiptStore async with isolated_reporting_pool(autocommit=True) as pool: store = PgReportingReceiptStore(pool=pool, clock=clock, notifications=notifications) diff --git a/tests/conformance/reporting/_reconciliation_support.py b/tests/conformance/reporting/_reconciliation_support.py index acbe7d7ef..11fdf1c4a 100644 --- a/tests/conformance/reporting/_reconciliation_support.py +++ b/tests/conformance/reporting/_reconciliation_support.py @@ -2,6 +2,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import hashlib from collections.abc import AsyncIterator from dataclasses import dataclass, replace @@ -10,10 +22,14 @@ import pytest +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore +else: + from adcp_reporting_storage import PgReportingReconciliationStore + from adcp.reporting.canonical_json import canonical_json_utf8_v1 from adcp.reporting.ledger import ( InMemoryReportingReconciliationStore, - PgReportingReconciliationStore, ReportingCanonicalDigest, ReportingControlTotalRecord, ReportingDeliveryScope, diff --git a/tests/conformance/reporting/_reliable_process.py b/tests/conformance/reporting/_reliable_process.py index b2d3659e9..7778fa31f 100644 --- a/tests/conformance/reporting/_reliable_process.py +++ b/tests/conformance/reporting/_reliable_process.py @@ -7,6 +7,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import base64 import json @@ -17,15 +29,15 @@ from datetime import timedelta from types import SimpleNamespace +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgReportingOutbox +else: + from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore from psycopg import AsyncConnection from psycopg_pool import AsyncConnectionPool -from adcp.reporting.ledger import PgReportingReconciliationStore -from adcp.reporting.outbox import ( - PgReportingOutbox, - ReportingEnvelopeCipher, - ReportingNotificationWorker, -) +from adcp.reporting.outbox import ReportingEnvelopeCipher, ReportingNotificationWorker from ._generation_support import configuration, obligation_for, revision_for from ._reliable_support import ( diff --git a/tests/conformance/reporting/_reliable_support.py b/tests/conformance/reporting/_reliable_support.py index c385f6c9d..81caed671 100644 --- a/tests/conformance/reporting/_reliable_support.py +++ b/tests/conformance/reporting/_reliable_support.py @@ -9,6 +9,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import hashlib import json @@ -23,6 +35,12 @@ import pytest +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgReportingOutbox +else: + from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore + from adcp.reporting.canonical_json import canonical_json_utf8_v1 from adcp.reporting.fixtures import ( OFFICIAL_OFFERING_ID, @@ -40,7 +58,6 @@ ) from adcp.reporting.ledger import ( InMemoryReportingReconciliationStore, - PgReportingReconciliationStore, ProducerOfferings, ReportingCanonicalDigest, ReportingConfiguration, @@ -63,7 +80,6 @@ ) from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationSubscription, ReportingNotificationWorker, diff --git a/tests/conformance/reporting/_row_storage_interop.py b/tests/conformance/reporting/_row_storage_interop.py index bcf00917c..bc630b832 100644 --- a/tests/conformance/reporting/_row_storage_interop.py +++ b/tests/conformance/reporting/_row_storage_interop.py @@ -10,22 +10,49 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import base64 import json from pathlib import Path from typing import Any -from adcp.reporting.ledger.row_storage import ( - ReportingObjectRowBindingV1, - ReportingPostgresRowBindingV1, - ReportingRowStorageOptionsV1, - ReportingRowStorageV1, -) -from adcp.reporting.ledger.row_storage_object import ( - ReportingRowObjectContextV1, - ReportingRowObjectPutInputV1, - create_filesystem_reporting_row_object_provider_v1, -) +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger.row_storage import ( + ReportingObjectRowBindingV1, + ReportingPostgresRowBindingV1, + ReportingRowStorageOptionsV1, + ReportingRowStorageV1, + ) +else: + from adcp_reporting_storage.ledger.row_storage import ( + ReportingObjectRowBindingV1, + ReportingPostgresRowBindingV1, + ReportingRowStorageOptionsV1, + ReportingRowStorageV1, + ) +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger.row_storage_object import ( + ReportingRowObjectContextV1, + ReportingRowObjectPutInputV1, + create_filesystem_reporting_row_object_provider_v1, + ) +else: + from adcp_reporting_storage.ledger.row_storage_object import ( + ReportingRowObjectContextV1, + ReportingRowObjectPutInputV1, + create_filesystem_reporting_row_object_provider_v1, + ) NAMESPACE = "interop" #: A zone whose local date differs from UTC, so the object key proves the timezone rule. @@ -273,3 +300,39 @@ async def replay_state(pool: Any, root: Path, state: dict[str, Any]) -> dict[str ), ) return versions + + +def write_fixture(path: Path, expected: list[dict[str, Any]], state: dict[str, Any]) -> None: + """Commit a writer's rows compactly: one row set, chunk, body or object per line. + + ``indent=1`` made each fixture 12,926 lines for 12 row sets (#1491); one + JSON item per line keeps a diff per row set and the file under 100 KB. + """ + + def items(values: list[dict[str, Any]]) -> str: + return ( + "[\n" + + ",\n".join( + " " + json.dumps(value, separators=(",", ":"), sort_keys=True) for value in values + ) + + "\n ]" + ) + + objects = ",\n".join( + f" {json.dumps(key)}:{json.dumps(value)}" + for key, value in sorted(state["objects"].items()) + ) + text = ( + "{\n" + ' "expected": ' + items(expected) + ",\n" + ' "state": {\n' + ' "chunk_bodies": ' + items(state["chunk_bodies"]).replace("\n ", "\n ") + ",\n" + ' "objects": {\n' + objects.replace("\n ", "\n ") + "\n },\n" + ' "row_chunks": ' + items(state["row_chunks"]).replace("\n ", "\n ") + ",\n" + ' "row_sets": ' + items(state["row_sets"]).replace("\n ", "\n ") + "\n" + " }\n" + "}\n" + ) + assert json.loads(text) == {"expected": expected, "state": state} + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) diff --git a/tests/conformance/reporting/_status_old_process.py b/tests/conformance/reporting/_status_old_process.py index 64d11ba96..4dbeb99ca 100644 --- a/tests/conformance/reporting/_status_old_process.py +++ b/tests/conformance/reporting/_status_old_process.py @@ -6,6 +6,18 @@ from __future__ import annotations +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import json import sys @@ -18,18 +30,24 @@ async def main(settings, release): source = Path(settings["old_source"]).resolve() sys.path.insert(0, str(source / "src")) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingLedgerStore + from adcp.reporting.outbox import PgReportingOutbox + else: + from adcp_reporting_storage import PgReportingLedgerStore, PgReportingOutbox + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox._schema import validate_schema + else: + from adcp_reporting_storage.outbox._schema import validate_schema from psycopg_pool import AsyncConnectionPool import adcp.reporting.ledger.notification_models as models import adcp.reporting.outbox.worker as worker_module - from adcp.reporting.ledger import PgReportingLedgerStore from adcp.reporting.outbox import ( - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationError, ReportingNotificationWorker, ) - from adcp.reporting.outbox._schema import validate_schema assert not hasattr(models, "StatusChanged") assert Path(worker_module.__file__).resolve().is_relative_to(source) diff --git a/tests/conformance/reporting/_status_process.py b/tests/conformance/reporting/_status_process.py index adfe639e8..26d242199 100644 --- a/tests/conformance/reporting/_status_process.py +++ b/tests/conformance/reporting/_status_process.py @@ -1,14 +1,30 @@ """Real database-clock roles in the shared process harness. No injected clock.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp on sys.path predates the storage split (#1510). + + Its stores still live in adcp.reporting and the storage package must not + be used. Evaluated at each import, after the script has set up sys.path. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + from dataclasses import replace from datetime import datetime, timedelta, timezone +if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.ledger import PgReportingReconciliationStore + from adcp.reporting.outbox import PgStatusNotificationStore +else: + from adcp_reporting_storage import PgReportingReconciliationStore, PgStatusNotificationStore from psycopg import AsyncConnection from psycopg_pool import AsyncConnectionPool -from adcp.reporting.ledger import PgReportingReconciliationStore, ReportingDeliveryEscalation +from adcp.reporting.ledger import ReportingDeliveryEscalation from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.outbox import PgStatusNotificationStore from .test_reporting_notification_outbox import statement @@ -65,7 +81,10 @@ async def execute(self, query, params=None, **kwargs): if settings.get("start_paused"): await barrier("status_worker_ready", backend_pid=pid) - import adcp.reporting.outbox.status_pg as module + if _storage_split(): # wheels before the storage split (#1510) + import adcp.reporting.outbox.status_pg as module + else: + import adcp_reporting_storage.outbox.status_pg as module enqueue, apply = module._enqueue_on, status._apply_on diff --git a/tests/conformance/reporting/test_pg_reporting_ledger.py b/tests/conformance/reporting/test_pg_reporting_ledger.py index 5840b0654..2de8aff41 100644 --- a/tests/conformance/reporting/test_pg_reporting_ledger.py +++ b/tests/conformance/reporting/test_pg_reporting_ledger.py @@ -42,6 +42,8 @@ allow_module_level=True, ) +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore # noqa: E402 + from adcp.reporting.canonical_json import canonical_json_utf8_v1 # noqa: E402 from adcp.reporting.ledger import ( # noqa: E402 ConsumerStatusIngest, @@ -59,7 +61,6 @@ derive_period, revision_content_sha256, ) -from adcp.reporting.ledger.pg import PgReportingLedgerStore # noqa: E402 ACCOUNT = "acct_pg" CALLER = ReportingStatusCaller(account_id=ACCOUNT, consumer_id="buyer_pg") diff --git a/tests/conformance/reporting/test_reliable_reporting_core_recovery.py b/tests/conformance/reporting/test_reliable_reporting_core_recovery.py index e91891871..3ddfe48a5 100644 --- a/tests/conformance/reporting/test_reliable_reporting_core_recovery.py +++ b/tests/conformance/reporting/test_reliable_reporting_core_recovery.py @@ -8,6 +8,7 @@ from typing import Any import pytest +from adcp_reporting_storage import postgres_reliable_reporting_service from adcp.reporting.fixtures import redacted_capabilities from adcp.reporting.ledger import InMemoryReportingLedgerStore, ReportingStatusCaller @@ -86,7 +87,7 @@ def memory(**kwargs: Any) -> ReliableReportingService: now = row[0] def postgres(**kwargs: Any) -> ReliableReportingService: - return ReliableReportingService.postgres(pool=pool, clock=lambda: now, **kwargs) + return postgres_reliable_reporting_service(pool=pool, clock=lambda: now, **kwargs) yield postgres, now diff --git a/tests/conformance/reporting/test_reliable_reporting_production_admission.py b/tests/conformance/reporting/test_reliable_reporting_production_admission.py index 0da31855b..4f005c41a 100644 --- a/tests/conformance/reporting/test_reliable_reporting_production_admission.py +++ b/tests/conformance/reporting/test_reliable_reporting_production_admission.py @@ -277,7 +277,7 @@ async def test_failed_context_admission_rolls_back_configuration_and_destination async def test_context_insert_guard_rejects_tampering_and_keeps_legacy_documents(tmp_path): raise_exception = pytest.importorskip("psycopg").errors.RaiseException - from adcp.reporting.production.schema import validate_production_schema + from adcp_reporting_storage.production.schema import validate_production_schema selected = {} async with production_harness( diff --git a/tests/conformance/reporting/test_reliable_reporting_service_lifecycle.py b/tests/conformance/reporting/test_reliable_reporting_service_lifecycle.py index 3e1048fda..403654744 100644 --- a/tests/conformance/reporting/test_reliable_reporting_service_lifecycle.py +++ b/tests/conformance/reporting/test_reliable_reporting_service_lifecycle.py @@ -9,10 +9,11 @@ from typing import Any import pytest +from adcp_reporting_storage import postgres_reliable_reporting_service +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore from adcp.reporting.fixtures import redacted_capabilities from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.ledger.pg import PgReportingLedgerStore from adcp.reporting.service import ( ReliableReportingService, ReliableReportingShutdownTimeoutError, @@ -174,7 +175,7 @@ async def close_owned() -> None: await asyncio.wait_for(service.close(), 5) assert configurations_at_close == [0 if cancel_call else 1] assert pool.closed is False # an injected PostgreSQL pool is borrowed - restarted = ReliableReportingService.postgres(pool=pool, account_context=_account_context) + restarted = postgres_reliable_reporting_service(pool=pool, account_context=_account_context) await restarted.start() try: retained = await restarted.store.list_configurations( diff --git a/tests/conformance/reporting/test_reporting_activity_migration.py b/tests/conformance/reporting/test_reporting_activity_migration.py index 1632a6c49..79c8f0197 100644 --- a/tests/conformance/reporting/test_reporting_activity_migration.py +++ b/tests/conformance/reporting/test_reporting_activity_migration.py @@ -12,16 +12,15 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgReportingOutbox +from adcp_reporting_storage.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema -from adcp.reporting.ledger import PgReportingLedgerStore from adcp.reporting.outbox import ( ActivityOutcome, ActivityRequest, - PgReportingOutbox, ReportingActivityProjector, ReportingNotificationError, ) -from adcp.reporting.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema from ._generation_support import ( NOW, @@ -42,7 +41,7 @@ from .test_reporting_notification_packaging import run_step from .test_reporting_webhook_activity import prepare, reserve, rows -SQL = files("adcp.reporting.ledger").joinpath("reporting_webhook_activity.sql").read_text() +SQL = files("adcp_reporting_storage.ledger").joinpath("reporting_webhook_activity.sql").read_text() # The integrated A predecessor includes checkpoint and locale-portable readiness. BASE = "17ee407ae3978c8a2bb54437287afbf9dafb8130" ROOT = Path(__file__).resolve().parents[3] @@ -51,13 +50,13 @@ async def install_a(pool): async with pool.connection() as conn, conn.transaction(): for name in CHAIN: - await conn.execute(files("adcp.reporting.ledger").joinpath(name).read_text()) + await conn.execute(files("adcp_reporting_storage.ledger").joinpath(name).read_text()) async def test_required_manifest_is_identical_across_random_schemas_and_repeated_migration(): snapshots = [] identities = [] - manifest = files("adcp.reporting.outbox").joinpath("required_schema.json").read_text() + manifest = files("adcp_reporting_storage.outbox").joinpath("required_schema.json").read_text() async with ( isolated_reporting_pool(autocommit=True) as first, isolated_reporting_pool(autocommit=True) as second, @@ -559,6 +558,7 @@ def actual_a_source(tmp_path_factory): values = json.load(sys.stdin) sys.path.insert(0, str(Path.cwd() / 'src')) from psycopg_pool import AsyncConnectionPool +# The actual A source predates the storage split: its stores live in adcp.reporting. from adcp.reporting.ledger import PgReportingLedgerStore from adcp.reporting.outbox import ( PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationSubscription, @@ -643,10 +643,12 @@ async def test_actual_a_binary_refuses_owned_schema_without_rewriting_delivery(a async def test_actual_a_schema_requires_ownership_maintenance_before_activity(actual_a_source): + from adcp_reporting_storage.migration import ( + ReportingOwnershipMigrationError, + migrate_legacy_reporting, + ) from psycopg import sql - from adcp.reporting.migration import ReportingOwnershipMigrationError, migrate_legacy_reporting - async with isolated_reporting_pool(autocommit=True) as pool: await run_a(actual_a_source, pool, action="bootstrap") ledger = PgReportingLedgerStore(pool=pool, notifications=True) diff --git a/tests/conformance/reporting/test_reporting_activity_schema_proof.py b/tests/conformance/reporting/test_reporting_activity_schema_proof.py index 5c6052197..175cdea91 100644 --- a/tests/conformance/reporting/test_reporting_activity_schema_proof.py +++ b/tests/conformance/reporting/test_reporting_activity_schema_proof.py @@ -1,5 +1,17 @@ """Real catalog accounting through the supported activity/capability composition.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp under test predates the storage split (#1510). + + The installed hardening controls run this module inside historical wheels, + whose stores still live in adcp.reporting. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio from concurrent.futures import ThreadPoolExecutor from contextlib import AsyncExitStack, asynccontextmanager, contextmanager @@ -106,8 +118,16 @@ async def compose_activity(reliable, composite): status = None reader = outbox if composite: - from adcp.reporting.outbox.status_activity_pg import PgReportingActivityUnionStore - from adcp.reporting.outbox.status_pg import PgStatusNotificationStore + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox.status_activity_pg import PgReportingActivityUnionStore + else: + from adcp_reporting_storage.outbox.status_activity_pg import ( + PgReportingActivityUnionStore, + ) + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox.status_pg import PgStatusNotificationStore + else: + from adcp_reporting_storage.outbox.status_pg import PgStatusNotificationStore store = PgStatusNotificationStore(reliable.store) await store.create_schema() @@ -461,7 +481,10 @@ async def test_separate_search_paths_and_instances_do_not_share_positive_proof(m async def test_b_and_composite_prove_each_packaged_contract_once(monkeypatch): - from adcp.reporting.outbox import _schema, status_schema + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import _schema, status_schema + else: + from adcp_reporting_storage.outbox import _schema, status_schema async with reliable_factory("postgres", notifications=True, autocommit=True) as reliable: composite = await compose_activity(reliable, True) @@ -514,7 +537,10 @@ def c_contract(installed, **options): async def test_positive_proof_is_bound_to_packaged_manifest_contract(activity_proof, monkeypatch): - from adcp.reporting.outbox import _schema + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.outbox import _schema + else: + from adcp_reporting_storage.outbox import _schema support, handler, accounting, _ = activity_proof await handler.get_adcp_capabilities() diff --git a/tests/conformance/reporting/test_reporting_buyer_submission_cas.py b/tests/conformance/reporting/test_reporting_buyer_submission_cas.py index 78b3fd3c2..8580e1fdb 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submission_cas.py +++ b/tests/conformance/reporting/test_reporting_buyer_submission_cas.py @@ -7,14 +7,14 @@ import threading import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore +from adcp_reporting_storage.submissions import pg as submission_pg from adcp.reporting.submissions import ( - PgReportingSubmissionIntentStore, ReportingSubmissionCode, ReportingSubmissionError, prepare_reporting_receipt_submission, ) -from adcp.reporting.submissions import pg as submission_pg from ._buyer_submission_support import SCOPE, mixed, receipt, response_for from ._generation_support import isolated_reporting_pool diff --git a/tests/conformance/reporting/test_reporting_buyer_submission_migration.py b/tests/conformance/reporting/test_reporting_buyer_submission_migration.py index 2794ce811..a2ce46c5b 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submission_migration.py +++ b/tests/conformance/reporting/test_reporting_buyer_submission_migration.py @@ -5,9 +5,9 @@ import traceback import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore from adcp.reporting.submissions import ( - PgReportingSubmissionIntentStore, ReportingSubmissionCode, ReportingSubmissionError, prepare_reporting_receipt_submission, diff --git a/tests/conformance/reporting/test_reporting_buyer_submission_packaging.py b/tests/conformance/reporting/test_reporting_buyer_submission_packaging.py index 0ef664db4..440465475 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submission_packaging.py +++ b/tests/conformance/reporting/test_reporting_buyer_submission_packaging.py @@ -12,15 +12,22 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore from adcp.reporting.ledger.delivery import receipt_to_wire -from adcp.reporting.submissions import PgReportingSubmissionIntentStore, ReportingSubmissionScope +from adcp.reporting.submissions import ReportingSubmissionScope from ._generation_support import require_rolling_database from ._receipt_support import adjustment_for, batch_state, receipt_case, receipt_harness from .test_reporting_buyer_submission_process import buyer_worker from .test_reporting_materializer_packaging import b1_wheels, built_distribution -from .test_reporting_notification_packaging import ROOT, run_step +from .test_reporting_notification_packaging import ( + ROOT, + STORAGE_SRC, + run_step, + storage_distribution, + storage_source, +) __all__ = ["b1_wheels", "built_distribution"] @@ -51,7 +58,12 @@ async def test_python310_installed_buyer_submission_contract(request, kind, driv ) await asyncio.to_thread( run_step, - [*installer, str(wheels[kind]) + ("[pg]" if drivers else ""), "mypy==1.20.2"], + [ + *installer, + str(wheels[kind]) + ("[pg]" if drivers else ""), + str(storage_distribution(wheels[kind])), + "mypy==1.20.2", + ], label=label + "-install", cwd=root, timeout=180, @@ -63,10 +75,20 @@ async def test_python310_installed_buyer_submission_contract(request, kind, driv ).hexdigest() for path in (ROOT / "src/adcp/reporting/submissions").glob("*.py") } - sql_path = ROOT / "src/adcp/reporting/ledger/reporting_buyer_submissions.sql" - with zipfile.ZipFile(wheels[kind]) as wheel, tarfile.open(source) as sdist: + modules["adcp_reporting_storage.submissions.pg"] = hashlib.sha256( + (STORAGE_SRC / "submissions/pg.py").read_bytes() + ).hexdigest() + sql_path = ( + ROOT + / "packages/adcp-reporting-storage/src/adcp_reporting_storage" + / "ledger/reporting_buyer_submissions.sql" + ) + with ( + zipfile.ZipFile(storage_distribution(wheels[kind])) as wheel, + tarfile.open(storage_source(source)) as sdist, + ): prefix = sdist.getnames()[0].split("/")[0] - member = "adcp/reporting/ledger/reporting_buyer_submissions.sql" + member = "adcp_reporting_storage/ledger/reporting_buyer_submissions.sql" assert ( wheel.read(member) == sdist.extractfile(f"{prefix}/src/{member}").read() diff --git a/tests/conformance/reporting/test_reporting_buyer_submission_process.py b/tests/conformance/reporting/test_reporting_buyer_submission_process.py index 96fb95b47..e43d60367 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submission_process.py +++ b/tests/conformance/reporting/test_reporting_buyer_submission_process.py @@ -8,9 +8,10 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore from adcp.reporting.ledger.delivery import receipt_to_wire -from adcp.reporting.submissions import PgReportingSubmissionIntentStore, ReportingSubmissionScope +from adcp.reporting.submissions import ReportingSubmissionScope from ._receipt_support import adjustment_for, batch_state, receipt_case, receipt_harness from .test_reporting_materializer_process import Child diff --git a/tests/conformance/reporting/test_reporting_buyer_submission_seller.py b/tests/conformance/reporting/test_reporting_buyer_submission_seller.py index 49b1a79d2..3e92f65e3 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submission_seller.py +++ b/tests/conformance/reporting/test_reporting_buyer_submission_seller.py @@ -3,11 +3,11 @@ from __future__ import annotations import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore from adcp.reporting.ledger.delivery import receipt_to_wire from adcp.reporting.submissions import ( InMemoryReportingSubmissionIntentStore, - PgReportingSubmissionIntentStore, ReportingReceiptFailureCode, ReportingSubmissionScope, submit_reporting_receipts, diff --git a/tests/conformance/reporting/test_reporting_buyer_submissions.py b/tests/conformance/reporting/test_reporting_buyer_submissions.py index 2ac1cb6be..403c7d817 100644 --- a/tests/conformance/reporting/test_reporting_buyer_submissions.py +++ b/tests/conformance/reporting/test_reporting_buyer_submissions.py @@ -8,10 +8,10 @@ from dataclasses import replace import pytest +from adcp_reporting_storage import PgReportingSubmissionIntentStore from adcp.reporting.canonical_json import canonical_json_utf8_v1 from adcp.reporting.submissions import ( - PgReportingSubmissionIntentStore, ReportingReceiptFailureCode, ReportingSubmissionCode, ReportingSubmissionError, diff --git a/tests/conformance/reporting/test_reporting_caller_ownership.py b/tests/conformance/reporting/test_reporting_caller_ownership.py index 5dade9351..7059e090d 100644 --- a/tests/conformance/reporting/test_reporting_caller_ownership.py +++ b/tests/conformance/reporting/test_reporting_caller_ownership.py @@ -8,6 +8,15 @@ from pathlib import Path import pytest +from adcp_reporting_storage import postgres_reliable_reporting_service +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore +from adcp_reporting_storage.migration import ( + ReportingOwnershipBackfill, + ReportingOwnershipMigrationError, + backfill_legacy_reporting, + legacy_generation_digest, + migrate_legacy_reporting, +) from adcp.exceptions import ADCPTaskError from adcp.reporting.ledger import ( @@ -17,15 +26,7 @@ ReportingStatusCaller, ReportingStatusHandler, ) -from adcp.reporting.ledger.pg import PgReportingLedgerStore from adcp.reporting.ledger.store import encode_cursor -from adcp.reporting.migration import ( - ReportingOwnershipBackfill, - ReportingOwnershipMigrationError, - backfill_legacy_reporting, - legacy_generation_digest, - migrate_legacy_reporting, -) from ._generation_support import ( NOW, @@ -173,10 +174,9 @@ async def test_maintenance_archive_backfill_preserves_evidence_and_never_replays import json require_rolling_database() + from adcp_reporting_storage.ledger.pg import _configuration_payload, _fingerprint from psycopg import sql - from adcp.reporting.ledger.pg import _configuration_payload, _fingerprint - archive = "adcp_reporting_quarantine_" + secrets.token_hex(6) async with isolated_reporting_pool(autocommit=True) as pool: async with pool.connection() as c: @@ -370,12 +370,11 @@ async def test_maintenance_archive_backfill_preserves_evidence_and_never_replays assert await store.list_all_configurations() == () # A new service process starts over retained data without asking # for source authority or constructing workers for the old work. - from adcp.reporting.service import ReliableReportingService def no_legacy_dispatch(configuration): raise AssertionError("quarantined generation cannot acquire source authority") - restarted = ReliableReportingService.postgres( + restarted = postgres_reliable_reporting_service( pool=pool, account_context=no_legacy_dispatch ) await restarted.start() @@ -493,7 +492,7 @@ async def check(service): else: async with isolated_reporting_pool() as pool: await check( - ReliableReportingService.postgres( + postgres_reliable_reporting_service( pool=pool, account_context=lambda _: None, caller_resolver=resolve ) ) diff --git a/tests/conformance/reporting/test_reporting_change_feed_consumers_pg.py b/tests/conformance/reporting/test_reporting_change_feed_consumers_pg.py index edf3a1a17..b7f719594 100644 --- a/tests/conformance/reporting/test_reporting_change_feed_consumers_pg.py +++ b/tests/conformance/reporting/test_reporting_change_feed_consumers_pg.py @@ -11,9 +11,8 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.ledger.change_feed import encode_reporting_change_cursor_v1 +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.change_feed import encode_reporting_change_cursor_v1 from ._generation_support import isolated_reporting_pool from .test_reporting_row_storage_pg import fetch_all diff --git a/tests/conformance/reporting/test_reporting_change_feed_horizon_pg.py b/tests/conformance/reporting/test_reporting_change_feed_horizon_pg.py index 9a4aaf99d..9948666ec 100644 --- a/tests/conformance/reporting/test_reporting_change_feed_horizon_pg.py +++ b/tests/conformance/reporting/test_reporting_change_feed_horizon_pg.py @@ -16,9 +16,8 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.ledger.change_feed import ( +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.change_feed import ( ReportingChangeCursorExpiredError, encode_reporting_change_cursor_v1, ) diff --git a/tests/conformance/reporting/test_reporting_change_feed_pg.py b/tests/conformance/reporting/test_reporting_change_feed_pg.py index 8b2f74c6e..fb3bcbe4a 100644 --- a/tests/conformance/reporting/test_reporting_change_feed_pg.py +++ b/tests/conformance/reporting/test_reporting_change_feed_pg.py @@ -6,9 +6,8 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.ledger.change_feed import ( +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.change_feed import ( ReportingChangeCursorExpiredError, decode_reporting_change_cursor_v1, encode_reporting_change_cursor_v1, @@ -367,7 +366,7 @@ async def test_a_committed_prefix_is_returned_and_the_cursor_stops_before_the_fi async def test_the_account_feed_query_is_served_by_the_account_seq_index() -> None: - from adcp.reporting.ledger.change_feed import _page_query + from adcp_reporting_storage.ledger.change_feed import _page_query async with isolated_reporting_pool() as pool: await feed_store(pool) diff --git a/tests/conformance/reporting/test_reporting_conditional_metrics.py b/tests/conformance/reporting/test_reporting_conditional_metrics.py index 2f561ce37..2398781cb 100644 --- a/tests/conformance/reporting/test_reporting_conditional_metrics.py +++ b/tests/conformance/reporting/test_reporting_conditional_metrics.py @@ -9,6 +9,7 @@ from typing import Any import pytest +from adcp_reporting_storage import postgres_reliable_reporting_service from adcp.reporting.conformance import ( ReportingSourceConformanceError, @@ -143,7 +144,7 @@ def context(config: ReportingConfiguration) -> ReportingAccountContext: async with AsyncExitStack() as stack: if request.param == "postgres": pool = await stack.enter_async_context(isolated_reporting_pool()) - service = ReliableReportingService.postgres( + service = postgres_reliable_reporting_service( pool=pool, account_context=context, clock=clock ) else: diff --git a/tests/conformance/reporting/test_reporting_core_lifecycle.py b/tests/conformance/reporting/test_reporting_core_lifecycle.py index 2dcfdb4d0..88a45b68c 100644 --- a/tests/conformance/reporting/test_reporting_core_lifecycle.py +++ b/tests/conformance/reporting/test_reporting_core_lifecycle.py @@ -3,7 +3,7 @@ Every other test in this suite checks one piece. This one checks that the pieces compose: a simulated reporting source drives :class:`~adcp.reporting.ledger.ReportingProducer`, which writes a -:class:`~adcp.reporting.ledger.pg.PgReportingLedgerStore`, which +:class:`~adcp_reporting_storage.ledger.pg.PgReportingLedgerStore`, which :class:`~adcp.reporting.ledger.ReportingStatusHandler` projects onto the wire, which the SDK's **own buyer-side** ``reconcile_reporting_core`` then reconciles. @@ -54,6 +54,8 @@ allow_module_level=True, ) +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore # noqa: E402 + from adcp.reporting import ( # noqa: E402 ExpectedReportingPeriod, ReportingReconciliationError, @@ -81,7 +83,6 @@ first_ordinal_after, iso_duration_to_timedelta, ) -from adcp.reporting.ledger.pg import PgReportingLedgerStore # noqa: E402 from adcp.reporting.ledger.producer import recompute_revision_content_sha256 # noqa: E402 from adcp.reporting.source import reporting_source_capabilities_sha256_v1 # noqa: E402 from adcp.types import ( # noqa: E402 diff --git a/tests/conformance/reporting/test_reporting_currency.py b/tests/conformance/reporting/test_reporting_currency.py index 77a85f080..18bdec28c 100644 --- a/tests/conformance/reporting/test_reporting_currency.py +++ b/tests/conformance/reporting/test_reporting_currency.py @@ -10,6 +10,7 @@ from typing import Any import pytest +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore from adcp.reporting import ( ReportingContentReading, @@ -55,7 +56,6 @@ revision_content_sha256, ) from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.ledger.pg import PgReportingLedgerStore from adcp.reporting.ledger.producer import recompute_revision_content_sha256 from adcp.reporting.source import ( ReportingSourceCapabilitiesV1, diff --git a/tests/conformance/reporting/test_reporting_currency_migration.py b/tests/conformance/reporting/test_reporting_currency_migration.py index fb2b0c670..321f0a3d3 100644 --- a/tests/conformance/reporting/test_reporting_currency_migration.py +++ b/tests/conformance/reporting/test_reporting_currency_migration.py @@ -8,13 +8,13 @@ from typing import TYPE_CHECKING, Any import pytest +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore from adcp.reporting.ledger import ( ProducerOfferings, ReportingProducer, ReportingStatusCaller, ) -from adcp.reporting.ledger.pg import PgReportingLedgerStore from ._generation_support import ( NOW, @@ -29,8 +29,10 @@ from psycopg_pool import AsyncConnectionPool FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" -MIGRATION = files("adcp.reporting.ledger").joinpath("reporting_ledger_obligation_currency.sql") -ACCOUNT_MIGRATION = files("adcp.reporting.ledger").joinpath( +MIGRATION = files("adcp_reporting_storage.ledger").joinpath( + "reporting_ledger_obligation_currency.sql" +) +ACCOUNT_MIGRATION = files("adcp_reporting_storage.ledger").joinpath( "reporting_ledger_account_generations.sql" ) @@ -74,12 +76,11 @@ async def test_migration_preserves_evidence_and_quarantines_unknown_currency( autocommit: bool, ) -> None: require_rolling_database() - from psycopg import sql - - from adcp.reporting.migration import ( + from adcp_reporting_storage.migration import ( ReportingOwnershipMigrationError, migrate_legacy_reporting, ) + from psycopg import sql async with isolated_reporting_pool(autocommit=autocommit) as pool: async with pool.connection() as connection: @@ -160,7 +161,7 @@ async def test_currency_constraints_and_immutability_cover_direct_sql() -> None: async def test_unexpected_default_rolls_back_without_rewriting_evidence() -> None: async with isolated_reporting_pool() as pool: - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with pool.connection() as connection: await connection.execute((FIXTURES / "reporting_ledger_beta15.sql").read_text()) diff --git a/tests/conformance/reporting/test_reporting_empty_legacy_replacement.py b/tests/conformance/reporting/test_reporting_empty_legacy_replacement.py index d1611e561..df06b25f8 100644 --- a/tests/conformance/reporting/test_reporting_empty_legacy_replacement.py +++ b/tests/conformance/reporting/test_reporting_empty_legacy_replacement.py @@ -7,9 +7,8 @@ from pathlib import Path import pytest - -from adcp.reporting.ledger.pg import PgReportingLedgerStore -from adcp.reporting.migration import ( +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore +from adcp_reporting_storage.migration import ( ReportingOwnershipMigrationError, replace_empty_legacy_reporting, ) diff --git a/tests/conformance/reporting/test_reporting_empty_legacy_replacement_sync.py b/tests/conformance/reporting/test_reporting_empty_legacy_replacement_sync.py index 07610a912..1b5922c6f 100644 --- a/tests/conformance/reporting/test_reporting_empty_legacy_replacement_sync.py +++ b/tests/conformance/reporting/test_reporting_empty_legacy_replacement_sync.py @@ -3,8 +3,7 @@ from __future__ import annotations import pytest - -from adcp.reporting.migration import ( +from adcp_reporting_storage.migration import ( ReportingOwnershipMigrationError, replace_empty_legacy_reporting_sync, ) diff --git a/tests/conformance/reporting/test_reporting_empty_owned_replacement.py b/tests/conformance/reporting/test_reporting_empty_owned_replacement.py index d24145da5..a264d2415 100644 --- a/tests/conformance/reporting/test_reporting_empty_owned_replacement.py +++ b/tests/conformance/reporting/test_reporting_empty_owned_replacement.py @@ -5,8 +5,7 @@ import asyncio import pytest - -from adcp.reporting.migration import ( +from adcp_reporting_storage.migration import ( ReportingOwnershipMigrationError, replace_empty_legacy_reporting, replace_empty_owned_reporting, @@ -256,7 +255,7 @@ def test_snapshot_isolation_refuses_rollback(isolation): async def test_async_rollback_matches_the_sync_guarantees(): require_rolling_database() - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore async with isolated_reporting_pool(autocommit=True) as pool: await PgReportingLedgerStore(pool=pool, change_feed=True).create_schema() diff --git a/tests/conformance/reporting/test_reporting_evidence_currency_integration.py b/tests/conformance/reporting/test_reporting_evidence_currency_integration.py index 7b21916d8..ecac503b8 100644 --- a/tests/conformance/reporting/test_reporting_evidence_currency_integration.py +++ b/tests/conformance/reporting/test_reporting_evidence_currency_integration.py @@ -907,15 +907,14 @@ async def test_populated_ledger_upgrade_then_granular_publication_and_exact_reco await connection.execute((fixture / "reporting_ledger_beta15.sql").read_text()) await connection.execute((fixture / "reporting_ledger_beta15_data.sql").read_text()) before = await _retained_rows(h.blobs.pool) - from psycopg import sql - - from adcp.reporting.migration import ( + from adcp_reporting_storage.migration import ( ReportingOwnershipBackfill, ReportingOwnershipMigrationError, backfill_legacy_reporting, legacy_generation_digest, migrate_legacy_reporting, ) + from psycopg import sql with pytest.raises(ReportingOwnershipMigrationError, match="Stop reporting workers"): await h.store.create_schema() @@ -1136,8 +1135,9 @@ async def test_postgres_fresh_process_reads_exact_seals_currency_rows_and_receip from psycopg_pool import AsyncConnectionPool from adcp.reporting.inline_source import InlineFetchResult, MetricEvidence from adcp.reporting.ledger import ( - PgReportingReconciliationStore, ReportingDeliveryPrincipal, ReportingReceiptKey, + ReportingDeliveryPrincipal, ReportingReceiptKey, ) +from adcp_reporting_storage import PgReportingReconciliationStore from adcp.reporting.source import ReportingSourceSliceRequestV1 from tests.conformance.reporting._reliable_support import ( ManualClock, ReliableHarness, _BytesStore, verified, diff --git a/tests/conformance/reporting/test_reporting_feed_hardening_installed.py b/tests/conformance/reporting/test_reporting_feed_hardening_installed.py index dc039aa1a..783cbd1db 100644 --- a/tests/conformance/reporting/test_reporting_feed_hardening_installed.py +++ b/tests/conformance/reporting/test_reporting_feed_hardening_installed.py @@ -62,6 +62,8 @@ def approved_b23(tmp_path_factory, request): with zipfile.ZipFile(wheel) as archive: modules = {} for name in set(feed_modules()) | (set(MODULES) - {"adcp.reporting.receipts._diagnostics"}): + # B23 predates the storage split: its stores live under adcp/reporting. + name = name.replace("adcp_reporting_storage.", "adcp.reporting.", 1) path = name.replace(".", "/") + ".py" if path not in archive.namelist(): path = name.replace(".", "/") + "/__init__.py" diff --git a/tests/conformance/reporting/test_reporting_feed_history.py b/tests/conformance/reporting/test_reporting_feed_history.py index 66e5e92ff..51a1bf6f2 100644 --- a/tests/conformance/reporting/test_reporting_feed_history.py +++ b/tests/conformance/reporting/test_reporting_feed_history.py @@ -342,7 +342,7 @@ async def test_accepted_artifact_and_canonical_evidence_remain_frozen_after_late # preserved. Actual unmodified DB-time selection has a separate vector. h.clock.now += timedelta(days=500) if h.pool is not None: - from adcp.reporting.feed import pg as feed_pg + from adcp_reporting_storage.feed import pg as feed_pg async def future(connection): return ( diff --git a/tests/conformance/reporting/test_reporting_feed_installed_pg.py b/tests/conformance/reporting/test_reporting_feed_installed_pg.py index 728256222..c0d73b76c 100644 --- a/tests/conformance/reporting/test_reporting_feed_installed_pg.py +++ b/tests/conformance/reporting/test_reporting_feed_installed_pg.py @@ -11,8 +11,8 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingFeedStore -from adcp.reporting.feed import PgReportingFeedStore from adcp.reporting.ledger import ReportingMaterializationCheck from adcp.reporting.ledger.status_projection import mismatch_key @@ -30,6 +30,7 @@ ) from .test_reporting_feed_process import feed_process from .test_reporting_notification_outbox import statement +from .test_reporting_notification_packaging import storage_distribution __all__ = ["b1_wheels", "built_distribution", "feed_wheels"] @@ -52,7 +53,12 @@ def installed_feed(request): else [str(python), "-m", "pip", "install"] ) run_step( - [*installer, f"{wheels[request.param]}[pg]", "asgi-lifespan==2.1.0"], + [ + *installer, + f"{wheels[request.param]}[pg]", + str(storage_distribution(wheels[request.param])), + "asgi-lifespan==2.1.0", + ], label=f"feed-{request.param}-pg-install", cwd=root, timeout=180, diff --git a/tests/conformance/reporting/test_reporting_feed_migration.py b/tests/conformance/reporting/test_reporting_feed_migration.py index 3a92343af..07ff2a81d 100644 --- a/tests/conformance/reporting/test_reporting_feed_migration.py +++ b/tests/conformance/reporting/test_reporting_feed_migration.py @@ -5,12 +5,12 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingFeedStore, PgReportingReceiptStore +from adcp_reporting_storage.outbox._schema import REQUIRED_OBJECTS, schema_objects +from adcp_reporting_storage.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.feed import PgReportingFeedStore, ReportingFeedError +from adcp.reporting.feed import ReportingFeedError from adcp.reporting.materializer import ReportingMaterializerLease -from adcp.reporting.outbox._schema import REQUIRED_OBJECTS, schema_objects -from adcp.reporting.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.receipts import PgReportingReceiptStore from ._durable_materializer_support import DurableHarness, durable_case from ._feed_support import feed_harness, feed_request, mixed_case, walk, without_feed @@ -19,8 +19,10 @@ from ._receipt_support import receipt_case, request_for from ._reconciliation_support import Clock -SQL = files("adcp.reporting.ledger").joinpath("reporting_feed.sql").read_text() -MANIFEST = json.loads(files("adcp.reporting.feed").joinpath("required_schema.json").read_text()) +SQL = files("adcp_reporting_storage.ledger").joinpath("reporting_feed.sql").read_text() +MANIFEST = json.loads( + files("adcp_reporting_storage.feed").joinpath("required_schema.json").read_text() +) async def fairness(pool): @@ -73,7 +75,9 @@ async def test_feed_migration_preserves_parent_catalog_receipts_pending_and_fair for package in ("materializer", "receipts"): parent_manifest.update( json.loads( - files("adcp.reporting." + package).joinpath("required_schema.json").read_text() + files("adcp_reporting_storage." + package) + .joinpath("required_schema.json") + .read_text() ) ) assert len(parent_manifest) == 767 @@ -94,7 +98,9 @@ async def test_feed_migration_preserves_parent_catalog_receipts_pending_and_fair assert len(MANIFEST) == 33 for package, count in (("materializer", 187), ("receipts", 102)): required = json.loads( - files("adcp.reporting." + package).joinpath("required_schema.json").read_text() + files("adcp_reporting_storage." + package) + .joinpath("required_schema.json") + .read_text() ) assert len(required) == count and all(actual[k] == v for k, v in required.items()) assert without_feed(await h.image()) == before diff --git a/tests/conformance/reporting/test_reporting_feed_packaging.py b/tests/conformance/reporting/test_reporting_feed_packaging.py index 36fef48c3..532e75a82 100644 --- a/tests/conformance/reporting/test_reporting_feed_packaging.py +++ b/tests/conformance/reporting/test_reporting_feed_packaging.py @@ -12,6 +12,11 @@ import pytest from .test_reporting_materializer_packaging import ROOT, b1_wheels, built_distribution, run_step +from .test_reporting_notification_packaging import ( + STORAGE_SRC, + storage_distribution, + storage_source, +) __all__ = ["b1_wheels", "built_distribution"] @@ -43,9 +48,10 @@ def feed_modules(): @pytest.fixture(scope="module") def feed_wheels(b1_wheels, built_distribution): root, wheels, _ = b1_wheels - _, _, source = built_distribution + _, _, adcp_source = built_distribution + source = storage_source(adcp_source) assets = { - name: (ROOT / "src/adcp/reporting" / name).read_bytes() + name: (STORAGE_SRC / name).read_bytes() for name in ( "ledger/reporting_feed.sql", "feed/required_schema.json", @@ -58,11 +64,11 @@ def feed_wheels(b1_wheels, built_distribution): with tarfile.open(source) as sdist: prefix = sdist.getnames()[0].split("/")[0] for name, raw in assets.items(): - assert sdist.extractfile(f"{prefix}/src/adcp/reporting/{name}").read() == raw + assert sdist.extractfile(f"{prefix}/src/adcp_reporting_storage/{name}").read() == raw for wheel in wheels.values(): - with zipfile.ZipFile(wheel) as archive: + with zipfile.ZipFile(storage_distribution(wheel)) as archive: for name, raw in assets.items(): - assert archive.read("adcp/reporting/" + name) == raw + assert archive.read("adcp_reporting_storage/" + name) == raw return root, wheels, {name: hashlib.sha256(raw).hexdigest() for name, raw in assets.items()} @@ -87,7 +93,7 @@ def test_python310_feed_without_pg_exports_sql_and_strict_adopter(request, kind) else [str(python), "-m", "pip", "install"] ) run_step( - [*installer, str(wheels[kind]), "mypy==1.20.2"], + [*installer, str(wheels[kind]), str(storage_distribution(wheels[kind])), "mypy==1.20.2"], label=f"feed-{kind}-base-install", cwd=root, timeout=180, diff --git a/tests/conformance/reporting/test_reporting_feed_transactions.py b/tests/conformance/reporting/test_reporting_feed_transactions.py index 69cffe0ab..0da75074c 100644 --- a/tests/conformance/reporting/test_reporting_feed_transactions.py +++ b/tests/conformance/reporting/test_reporting_feed_transactions.py @@ -10,9 +10,10 @@ from uuid import UUID import pytest +from adcp_reporting_storage import PgReportingFeedStore from adcp.reporting.canonical_json import canonical_json_utf8_v1 -from adcp.reporting.feed import PgReportingFeedStore, ReportingFeedError +from adcp.reporting.feed import ReportingFeedError from adcp.reporting.feed.snapshot import StoredFeedSnapshot from ._durable_materializer_support import DurableHarness, durable_case @@ -37,7 +38,7 @@ @asynccontextmanager async def paused_feed_projection(monkeypatch, *, failure=False): """Pause the real pure projector, without blocking the test event loop.""" - from adcp.reporting.feed import pg + from adcp_reporting_storage.feed import pg loop = asyncio.get_running_loop() loop_thread = threading.get_ident() @@ -432,7 +433,9 @@ def failing(*args, **kwargs): if fault == "assembly": monkeypatch.setattr(StoredFeedSnapshot, "page", failing) else: - from adcp.reporting.feed import memory, pg + from adcp_reporting_storage.feed import pg + + from adcp.reporting.feed import memory monkeypatch.setattr(memory if h.pool is None else pg, "inject_context", failing) elif h.pool is None: diff --git a/tests/conformance/reporting/test_reporting_feed_transports.py b/tests/conformance/reporting/test_reporting_feed_transports.py index f0b89f5b7..79d91ab6d 100644 --- a/tests/conformance/reporting/test_reporting_feed_transports.py +++ b/tests/conformance/reporting/test_reporting_feed_transports.py @@ -346,10 +346,11 @@ async def forbidden_projection(*args, **kwargs): async def test_feed_mount_preserves_closed_tier_and_new_store_notification_admission(feeds): + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationWorker, ) diff --git a/tests/conformance/reporting/test_reporting_generation_identity.py b/tests/conformance/reporting/test_reporting_generation_identity.py index 81145e450..d8ed953e7 100644 --- a/tests/conformance/reporting/test_reporting_generation_identity.py +++ b/tests/conformance/reporting/test_reporting_generation_identity.py @@ -8,6 +8,8 @@ from datetime import timedelta import pytest +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore +from adcp_reporting_storage.outbox._schema import schema_objects from adcp.reporting.fixtures import redacted_capabilities from adcp.reporting.inline_source import InlineReportingSource @@ -26,8 +28,6 @@ consumer_mismatch_issue_key, ) from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.ledger.pg import PgReportingLedgerStore -from adcp.reporting.outbox._schema import schema_objects from adcp.reporting.source import ReportingSourceSliceRequestV1 from tests.conformance.reporting._generation_support import ( END, diff --git a/tests/conformance/reporting/test_reporting_generation_migration.py b/tests/conformance/reporting/test_reporting_generation_migration.py index f4a2766b8..24c6f096f 100644 --- a/tests/conformance/reporting/test_reporting_generation_migration.py +++ b/tests/conformance/reporting/test_reporting_generation_migration.py @@ -9,8 +9,8 @@ from typing import TYPE_CHECKING, Any import pytest +from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore -from adcp.reporting.ledger.pg import PgReportingLedgerStore from tests.conformance.reporting._generation_support import ( isolated_reporting_pool, ) @@ -23,7 +23,9 @@ _FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" _BETA15_SCHEMA = _FIXTURES / "reporting_ledger_beta15.sql" _BETA15_DATA = _FIXTURES / "reporting_ledger_beta15_data.sql" -_MIGRATION = files("adcp.reporting.ledger").joinpath("reporting_ledger_account_generations.sql") +_MIGRATION = files("adcp_reporting_storage.ledger").joinpath( + "reporting_ledger_account_generations.sql" +) _TABLES = ( "reporting_configurations", "reporting_obligations", @@ -142,7 +144,7 @@ async def _other_constraints(pool: AsyncConnectionPool) -> list[Any]: async def test_beta15_boot_fails_closed_and_preserves_all_evidence() -> None: - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with isolated_reporting_pool() as pool: await _load_beta15(pool) @@ -189,7 +191,7 @@ async def test_standalone_key_upgrade_requires_explicit_maintenance() -> None: ], ) async def test_legacy_key_variants_do_not_authorize_implicit_ownership(key_change: str) -> None: - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with isolated_reporting_pool() as pool: await _load_beta15(pool) @@ -202,7 +204,7 @@ async def test_legacy_key_variants_do_not_authorize_implicit_ownership(key_chang async def test_adopter_foreign_keys_survive_refused_implicit_upgrade() -> None: - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with isolated_reporting_pool() as pool: await _load_beta15(pool) diff --git a/tests/conformance/reporting/test_reporting_host_reads_pg.py b/tests/conformance/reporting/test_reporting_host_reads_pg.py index 284bb7779..a508d5e45 100644 --- a/tests/conformance/reporting/test_reporting_host_reads_pg.py +++ b/tests/conformance/reporting/test_reporting_host_reads_pg.py @@ -8,8 +8,7 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore +from adcp_reporting_storage import PgReportingLedgerStore from ._generation_support import START, configuration, isolated_reporting_pool, obligation_for from .test_reporting_row_storage_pg import fetch_all, many_rows, typed_revision diff --git a/tests/conformance/reporting/test_reporting_inline_storage.py b/tests/conformance/reporting/test_reporting_inline_storage.py index c061578a4..6bb765a50 100644 --- a/tests/conformance/reporting/test_reporting_inline_storage.py +++ b/tests/conformance/reporting/test_reporting_inline_storage.py @@ -22,6 +22,7 @@ import anyio import pytest +from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore from adcp.reporting.conformance import validate_reporting_source_execution from adcp.reporting.fixtures import redacted_capabilities, redacted_snapshot_request @@ -29,8 +30,6 @@ from adcp.reporting.inline_storage import ( INLINE_STAGING_MAX_BYTES, InlineStorageError, - PgReportingSealStore, - PgReportingStagingStore, PreparedBackendObjectV1, PreparedBackendSealV1, ) @@ -109,12 +108,12 @@ def find_spec(self, fullname, path=None, target=None): if fullname.split('.')[0] in {'psycopg', 'psycopg_pool'}: raise ModuleNotFoundError('driver deliberately absent') sys.meta_path.insert(0, MissingDriver()) -from adcp.reporting.inline_storage import PgReportingStagingStore, PgReportingSealStore +from adcp_reporting_storage.inline_storage import PgReportingStagingStore, PgReportingSealStore for cls in [PgReportingStagingStore, PgReportingSealStore]: try: cls(pool=object()) except ImportError as error: - assert str(error) == 'PostgreSQL inline storage requires adcp[pg]' + assert str(error) == 'PostgreSQL inline storage requires adcp-reporting-storage[pg]' assert error.__context__ is None else: raise AssertionError('construction requires the optional driver') @@ -691,7 +690,7 @@ async def test_unmigrated_operations_are_schema_unready(database, operation) -> async def test_catalog_additive_metadata_does_not_change_readiness(stores, monkeypatch) -> None: - import adcp.reporting.inline_storage as module + import adcp_reporting_storage.inline_storage as module staging, _, _ = stores original = module.schema_objects @@ -709,7 +708,7 @@ async def with_metadata(connection): @pytest.mark.parametrize("field", ["fingerprint", "enabled"]) async def test_catalog_required_fields_still_fail_closed(stores, monkeypatch, field) -> None: - import adcp.reporting.inline_storage as module + import adcp_reporting_storage.inline_storage as module staging, _, _ = stores original = module.schema_objects @@ -853,7 +852,7 @@ async def forged_transaction(): async def test_large_payload_hash_yields_and_matches_exact_bytes(stores, monkeypatch) -> None: - import adcp.reporting.inline_storage as module + import adcp_reporting_storage.inline_storage as module staging, _, _ = stores payload = b"x" * INLINE_STAGING_MAX_BYTES @@ -893,7 +892,7 @@ async def test_cancellation_during_payload_hashing_settles_without_partial_write ) -> None: staging, _, pool = stores payload = b"x" * INLINE_STAGING_MAX_BYTES - import adcp.reporting.inline_storage as module + import adcp_reporting_storage.inline_storage as module original_digest = module._payload_digest original_transaction = staging._transaction diff --git a/tests/conformance/reporting/test_reporting_materializer_durable.py b/tests/conformance/reporting/test_reporting_materializer_durable.py index 153c252d8..b865f3332 100644 --- a/tests/conformance/reporting/test_reporting_materializer_durable.py +++ b/tests/conformance/reporting/test_reporting_materializer_durable.py @@ -33,10 +33,11 @@ async def test_core_records_do_not_create_managed_work_capture_or_readiness(back async def test_durable_verified_success_still_cannot_advertise_unactivated_readiness(backend): + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationWorker, ) @@ -79,10 +80,11 @@ async def test_materializer_store_closes_core_advertisement_until_b24_admits_it( Managed/Reconciled claims. Fail closed is correct here, but it is broader than a tier veto and an adopter switching store classes must be told. """ + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationWorker, ) @@ -109,7 +111,7 @@ async def advertise(store, outbox): reviewed_outbox = InMemoryReportingOutbox(reviewed) outbox = InMemoryReportingOutbox(h.store) else: - from adcp.reporting.ledger.delivery_pg import PgReportingReconciliationStore + from adcp_reporting_storage.ledger.delivery_pg import PgReportingReconciliationStore reviewed = PgReportingReconciliationStore(pool=h.pool, notifications=True) reviewed_outbox = PgReportingOutbox(pool=h.pool) diff --git a/tests/conformance/reporting/test_reporting_materializer_installed_pg.py b/tests/conformance/reporting/test_reporting_materializer_installed_pg.py index 86e4d7e49..fe081d8a1 100644 --- a/tests/conformance/reporting/test_reporting_materializer_installed_pg.py +++ b/tests/conformance/reporting/test_reporting_materializer_installed_pg.py @@ -9,13 +9,13 @@ from pathlib import Path import pytest - -from adcp.reporting.materializer import PgReportingMaterializerStore +from adcp_reporting_storage import PgReportingMaterializerStore from ._durable_materializer_support import DurableHarness, durable_case from ._generation_support import isolated_reporting_pool, require_rolling_database from .test_reporting_materializer_packaging import ROOT, b1_wheels, built_distribution, run_step from .test_reporting_materializer_process import worker +from .test_reporting_notification_packaging import STORAGE_SRC, storage_distribution # Expose the exact existing fixtures without rebuilding a current module as an # alleged historical artifact. These are the new head's two distribution paths. @@ -41,7 +41,11 @@ def installed_materializer(request): else [str(python), "-m", "pip", "install"] ) run_step( - [*installer, f"{wheels[request.param]}[pg]"], + [ + *installer, + f"{wheels[request.param]}[pg]", + str(storage_distribution(wheels[request.param])), + ], label=f"materializer-{request.param}-pg-install", cwd=path, timeout=180, @@ -58,10 +62,13 @@ def installed_materializer(request): if os.environ.get("ADCP_PYTHON310"): assert version == [3, 10] modules = {} - for name in ("materializer/pg.py", "materializer/service.py", "materializer/verification.py"): + for name in ("materializer/service.py", "materializer/verification.py"): modules["adcp.reporting." + name.removesuffix(".py").replace("/", ".")] = hashlib.sha256( (ROOT / "src/adcp/reporting" / name).read_bytes() ).hexdigest() + modules["adcp_reporting_storage.materializer.pg"] = hashlib.sha256( + (STORAGE_SRC / "materializer/pg.py").read_bytes() + ).hexdigest() return path, python, script, {"workspace": str(ROOT), "python": version, "modules": modules} diff --git a/tests/conformance/reporting/test_reporting_materializer_migration.py b/tests/conformance/reporting/test_reporting_materializer_migration.py index 16872d0b8..2a6503df7 100644 --- a/tests/conformance/reporting/test_reporting_materializer_migration.py +++ b/tests/conformance/reporting/test_reporting_materializer_migration.py @@ -6,19 +6,19 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingMaterializerStore, PgReportingReconciliationStore +from adcp_reporting_storage.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema +from adcp_reporting_storage.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.ledger import LedgerConflictError, PgReportingReconciliationStore, derive_period -from adcp.reporting.materializer import PgReportingMaterializerStore -from adcp.reporting.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema -from adcp.reporting.outbox.status_schema import REQUIRED_STATUS_OBJECTS +from adcp.reporting.ledger import LedgerConflictError, derive_period from ._durable_materializer_support import durable_case, durable_harness from ._generation_support import isolated_reporting_pool, obligation_for from ._provisional_catalog import PROVISIONAL_OBJECTS -SQL = files("adcp.reporting.ledger").joinpath("reporting_materializer.sql").read_text() +SQL = files("adcp_reporting_storage.ledger").joinpath("reporting_materializer.sql").read_text() MANIFEST = json.loads( - files("adcp.reporting.materializer").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.materializer").joinpath("required_schema.json").read_text() ) @@ -251,8 +251,9 @@ async def test_bounded_sampling_walks_past_a_full_page_of_busy_account_locks(): async def test_pre_activation_event_never_promotes_and_old_outbox_cannot_claim_it(): + from adcp_reporting_storage import PgReportingOutbox + from adcp.reporting.ledger.notification_models import decode_event - from adcp.reporting.outbox import PgReportingOutbox async with durable_harness("postgres", notifications=True) as h: case = await durable_case(h.store) diff --git a/tests/conformance/reporting/test_reporting_materializer_packaging.py b/tests/conformance/reporting/test_reporting_materializer_packaging.py index 348c20cd6..b7ff195f2 100644 --- a/tests/conformance/reporting/test_reporting_materializer_packaging.py +++ b/tests/conformance/reporting/test_reporting_materializer_packaging.py @@ -43,11 +43,27 @@ def b1_wheels(built_distribution): timeout=180, ) vcs_wheel = next(direct.glob("*.whl")) + # The storage distribution's VCS wheel, from its own tree, next to adcp's. + storage_direct = path / "vcs-wheel-storage" + run_step( + distribution.build_command(distribution.STORAGE_ROOT, storage_direct, wheel=True), + label="b1-vcs-storage-wheel", + cwd=ROOT, + timeout=180, + ) + storage_vcs_wheel = next(storage_direct.glob("*.whl")) + distribution.register_storage( + vcs_wheel, + wheel=storage_vcs_wheel, + source=distribution.storage_source(sdist_wheel), + ) expected = {f.name: f.read_bytes() for f in ASSETS.glob("*.json")} with ( zipfile.ZipFile(vcs_wheel) as vcs, zipfile.ZipFile(sdist_wheel) as wheel, tarfile.open(source) as tar, + zipfile.ZipFile(storage_vcs_wheel) as storage_vcs, + zipfile.ZipFile(distribution.storage_distribution(sdist_wheel)) as storage_wheel, ): prefix = tar.getnames()[0].split("/")[0] for name, raw in expected.items(): @@ -63,9 +79,9 @@ def b1_wheels(built_distribution): "receipts/required_schema.json", ): assert ( - vcs.read(f"adcp/reporting/{relative}") - == wheel.read(f"adcp/reporting/{relative}") - == (ROOT / "src/adcp/reporting" / relative).read_bytes() + storage_vcs.read(f"adcp_reporting_storage/{relative}") + == storage_wheel.read(f"adcp_reporting_storage/{relative}") + == (distribution.STORAGE_SRC / relative).read_bytes() ) return ( path, @@ -123,7 +139,12 @@ def test_python310_installed_wheel_exports_verifier_reference_and_strict_adopter else [str(python), "-m", "pip", "install"] ) run_step( - [*installer, str(wheels[kind]), "mypy==1.20.2"], + [ + *installer, + str(wheels[kind]), + str(distribution.storage_distribution(wheels[kind])), + "mypy==1.20.2", + ], label=f"b1-{kind}-noneditable-install", cwd=path, timeout=180, diff --git a/tests/conformance/reporting/test_reporting_materializer_progress.py b/tests/conformance/reporting/test_reporting_materializer_progress.py index 39c82e7da..98154a0cf 100644 --- a/tests/conformance/reporting/test_reporting_materializer_progress.py +++ b/tests/conformance/reporting/test_reporting_materializer_progress.py @@ -7,12 +7,9 @@ from contextlib import asynccontextmanager import pytest +from adcp_reporting_storage import PgReportingMaterializerStore -from adcp.reporting.materializer import ( - PgReportingMaterializerStore, - ReportingMaterializerLease, - ReportingWriterError, -) +from adcp.reporting.materializer import ReportingMaterializerLease, ReportingWriterError from ._durable_materializer_support import durable_case diff --git a/tests/conformance/reporting/test_reporting_materializer_service.py b/tests/conformance/reporting/test_reporting_materializer_service.py index 97292ab4e..22e5d9971 100644 --- a/tests/conformance/reporting/test_reporting_materializer_service.py +++ b/tests/conformance/reporting/test_reporting_materializer_service.py @@ -59,7 +59,7 @@ async def test_cancellation_and_restart_never_allocate_a_new_external_identity( async def test_service_heartbeat_keeps_long_io_alive_with_size_one_postgres_pool( stage, tmp_path, monkeypatch ): - from adcp.reporting.materializer import PgReportingMaterializerStore + from adcp_reporting_storage import PgReportingMaterializerStore async with durable_harness("postgres", notifications=True) as h: from psycopg_pool import AsyncConnectionPool diff --git a/tests/conformance/reporting/test_reporting_materializer_transactions.py b/tests/conformance/reporting/test_reporting_materializer_transactions.py index 9788fba5a..7f04ac5a8 100644 --- a/tests/conformance/reporting/test_reporting_materializer_transactions.py +++ b/tests/conformance/reporting/test_reporting_materializer_transactions.py @@ -72,7 +72,7 @@ async def test_reservation_rolls_back_attempt_work_delivery_and_sequence_heads( # Count explicitly so attempt failure happens after the delivery's # own complete insertion, not at its earlier feed append. if position == "attempt": - from adcp.reporting.materializer.pg import PgReportingMaterializerStore + from adcp_reporting_storage.materializer.pg import PgReportingMaterializerStore original = PgReportingMaterializerStore._commit_record_on @@ -261,7 +261,7 @@ def empty(self, event): monkeypatch.setattr(MaterializerNotificationState, "enqueue", empty) else: - import adcp.reporting.materializer.pg as pg + import adcp_reporting_storage.materializer.pg as pg async def empty(connection, event): pass diff --git a/tests/conformance/reporting/test_reporting_notification_migration.py b/tests/conformance/reporting/test_reporting_notification_migration.py index b470363ce..0f34c401d 100644 --- a/tests/conformance/reporting/test_reporting_notification_migration.py +++ b/tests/conformance/reporting/test_reporting_notification_migration.py @@ -8,11 +8,11 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore +from adcp_reporting_storage.outbox._schema import schema_contract, validate_schema +from adcp_reporting_storage.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.ledger import PgReportingReconciliationStore -from adcp.reporting.outbox import PgReportingOutbox, ReportingNotificationError -from adcp.reporting.outbox._schema import schema_contract, validate_schema -from adcp.reporting.outbox.status_schema import REQUIRED_STATUS_OBJECTS +from adcp.reporting.outbox import ReportingNotificationError from ._generation_support import ( NOW, @@ -24,7 +24,7 @@ from ._reliable_support import Barrier from .test_reporting_reconciliation_migration import FIXTURES -RESOURCES = files("adcp.reporting.ledger") +RESOURCES = files("adcp_reporting_storage.ledger") CHAIN = ( "reporting_ledger.sql", "reporting_ledger_account_generations.sql", @@ -159,10 +159,12 @@ async def test_populated_pre_outbox_upgrade_never_rewrites_or_backfills(autocomm @pytest.mark.parametrize("hopwise", [False, True]) async def test_direct_and_hopwise_historical_schema_chain(source, hopwise): require_rolling_database() + from adcp_reporting_storage.migration import ( + ReportingOwnershipMigrationError, + migrate_legacy_reporting, + ) from psycopg import sql - from adcp.reporting.migration import ReportingOwnershipMigrationError, migrate_legacy_reporting - async with isolated_reporting_pool(autocommit=True) as pool: async with pool.connection() as conn: await conn.execute((FIXTURES / source).read_text()) @@ -314,7 +316,7 @@ async def test_readiness_validates_the_installed_chain_not_table_presence(damage async def test_malformed_outbox_upgrade_rolls_back_entire_chain(): async with isolated_reporting_pool(autocommit=True) as pool: - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with pool.connection() as conn: await conn.execute((FIXTURES / "reporting_ledger_beta15.sql").read_text()) @@ -357,7 +359,7 @@ async def test_default_off_upgrade_preserves_adopter_index_and_opt_in_checks_rea )[0] assert retained == original # B validates the required subset: an unrelated index remains compatible. - from adcp.reporting.outbox import PgReportingOutbox + from adcp_reporting_storage import PgReportingOutbox await PgReportingOutbox(pool=pool).create_schema() async with pool.connection() as conn: diff --git a/tests/conformance/reporting/test_reporting_notification_packaging.py b/tests/conformance/reporting/test_reporting_notification_packaging.py index 615786fb5..c5a1bdadc 100644 --- a/tests/conformance/reporting/test_reporting_notification_packaging.py +++ b/tests/conformance/reporting/test_reporting_notification_packaging.py @@ -31,6 +31,44 @@ from .test_reporting_notification_migration import CHAIN, WAIVER_OBJECTS ROOT = Path(__file__).resolve().parents[3] +#: The storage distribution ships separately (adcp-client-python#1510); a current +#: build is two wheels. Historical wheels predate the split and carry the stores. +STORAGE_ROOT = ROOT / "packages/adcp-reporting-storage" +STORAGE_SRC = STORAGE_ROOT / "src/adcp_reporting_storage" +#: adcp wheel or sdist of the current tree -> (storage wheel, storage sdist) built with it. +STORAGE_DISTRIBUTIONS: dict[Path, tuple[Path, Path]] = {} + + +def register_storage(*adcp_artifacts, wheel, source): + for artifact in adcp_artifacts: + STORAGE_DISTRIBUTIONS[Path(artifact)] = (Path(wheel), Path(source)) + + +def is_current_build(distribution): + """Whether ``distribution`` is an adcp artifact of this tree (so needs the storage wheel).""" + return Path(distribution) in STORAGE_DISTRIBUTIONS + + +def storage_distribution(distribution): + """The adcp-reporting-storage wheel to install next to this adcp artifact.""" + return STORAGE_DISTRIBUTIONS[Path(distribution)][0] + + +def storage_source(distribution): + return STORAGE_DISTRIBUTIONS[Path(distribution)][1] + + +def storage_sources(): + """The storage package inputs, for build fingerprints and copies.""" + return [ + p + for p in STORAGE_ROOT.rglob("*") + if p.is_file() + and not any( + part in {"__pycache__", "build", "dist"} or part.endswith(".egg-info") + for part in p.parts + ) + ] def build_command(project, output, *, wheel=False): @@ -291,6 +329,7 @@ def built_distribution(tmp_path_factory, request): ) sources.extend(p for p in (ROOT / "schemas/cache").rglob("*.json")) sources.extend(p for p in (ROOT / "schemas/releases").rglob("*.json")) + sources.extend(storage_sources()) digest = hashlib.sha256() for item in sorted(sources): digest.update(str(item.relative_to(ROOT)).encode()) @@ -304,6 +343,10 @@ def built_distribution(tmp_path_factory, request): "distribution source changed; rebuild the release artifact" ) wheel, source = next(cached.glob("*.whl")), next(cached.glob("*.tar.gz")) + storage = cached / "storage" + register_storage( + wheel, source, wheel=next(storage.glob("*.whl")), source=next(storage.glob("*.tar.gz")) + ) print("notification_distribution stage=reuse-verified-wheel-sdist passed", flush=True) return path, wheel, source project = path / "project" @@ -336,12 +379,34 @@ def built_distribution(tmp_path_factory, request): timeout=300, ) wheel, source = next(dist.glob("*.whl")), next(dist.glob("*.tar.gz")) + # The storage distribution builds from its own tree; its wheel declares the + # adcp range and installs next to the adcp wheel built above. + storage_project, storage_dist = path / "project-storage", path / "dist-storage" + shutil.copytree( + STORAGE_ROOT, + storage_project, + ignore=shutil.ignore_patterns("__pycache__", "*.egg-info", "build", "dist"), + ) + run_step( + build_command(storage_project, storage_dist), + label="build-storage-sdist-and-wheel", + cwd=path, + timeout=300, + ) + storage_wheel = next(storage_dist.glob("*.whl")) + storage_sdist = next(storage_dist.glob("*.tar.gz")) if cached is not None: cached.mkdir(parents=True, exist_ok=True) shutil.copy2(wheel, cached / wheel.name) shutil.copy2(source, cached / source.name) + (cached / "storage").mkdir(exist_ok=True) + shutil.copy2(storage_wheel, cached / "storage" / storage_wheel.name) + shutil.copy2(storage_sdist, cached / "storage" / storage_sdist.name) (cached / "source.sha256").write_text(fingerprint + "\n") wheel, source = cached / wheel.name, cached / source.name + storage_wheel = cached / "storage" / storage_wheel.name + storage_sdist = cached / "storage" / storage_sdist.name + register_storage(wheel, source, wheel=storage_wheel, source=storage_sdist) return path, wheel, source @@ -363,15 +428,19 @@ def installed_distribution(built_distribution, request): if shutil.which("uv") else [str(python), "-m", "pip", "install"] ) - run_step([*installer, str(distribution)], label=f"{request.param}-base-install", cwd=path) + run_step( + [*installer, str(distribution), str(storage_distribution(distribution))], + label=f"{request.param}-base-install", + cwd=path, + ) base_check = r""" import importlib.util from importlib.resources import files from pathlib import Path import adcp +from adcp_reporting_storage import PgReportingOutbox from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationWorker, ReportingActivityProjector, @@ -390,7 +459,7 @@ def installed_distribution(built_distribution, request): assert importlib.util.find_spec("psycopg") is None assert importlib.util.find_spec("psycopg_pool") is None -assert "adcp.reporting.outbox.status_pg" not in sys.modules +assert "adcp_reporting_storage.outbox.status_pg" not in sys.modules assert "installed" in str(Path(adcp.__file__)) assert "notifications" not in inspect.signature(ReportingProducer).parameters assert not hasattr(InMemoryReportingLedgerStore(), "commit_materialization") @@ -400,10 +469,10 @@ def installed_distribution(built_distribution, request): try: PgReportingOutbox(pool=None) except ImportError as error: - assert "adcp[pg]" in str(error), str(error) + assert "adcp-reporting-storage[pg]" in str(error), str(error) else: raise AssertionError("PgReportingOutbox must raise the [pg] install hint") -from adcp.reporting.outbox import ( +from adcp_reporting_storage import ( PgStatusNotificationStore, PgReportingStatusOutbox, PgReportingActivityUnionStore, ) for constructor in (lambda: PgStatusNotificationStore(None), @@ -412,16 +481,18 @@ def installed_distribution(built_distribution, request): try: constructor() except ImportError as error: - assert "adcp[pg]" in str(error) + assert "adcp-reporting-storage[pg]" in str(error) else: raise AssertionError("every C PG constructor must explain its extra") -assert files("adcp.reporting.ledger").joinpath("reporting_notification_outbox.sql").is_file() -assert files("adcp.reporting.ledger").joinpath("reporting_webhook_activity.sql").is_file() -assert files("adcp.reporting.outbox").joinpath("required_schema.json").is_file() -assert files("adcp.reporting.outbox").joinpath("required_status_schema.json").is_file() -assert files("adcp.reporting.ledger").joinpath("reporting_status_notifications.sql").is_file() -assert files("adcp.reporting.ledger").joinpath("reporting_status_selector_version.sql").is_file() -assert files("adcp.reporting.outbox").joinpath("required_status_selector_schema.json").is_file() +ledger_assets = files("adcp_reporting_storage.ledger") +outbox_assets = files("adcp_reporting_storage.outbox") +assert ledger_assets.joinpath("reporting_notification_outbox.sql").is_file() +assert ledger_assets.joinpath("reporting_webhook_activity.sql").is_file() +assert outbox_assets.joinpath("required_schema.json").is_file() +assert outbox_assets.joinpath("required_status_schema.json").is_file() +assert ledger_assets.joinpath("reporting_status_notifications.sql").is_file() +assert ledger_assets.joinpath("reporting_status_selector_version.sql").is_file() +assert outbox_assets.joinpath("required_status_selector_schema.json").is_file() version = files("adcp").joinpath("ADCP_VERSION").read_text().strip() assert get_named_validator( "core/reporting-status-changed-webhook.json", version=version) is not None @@ -440,7 +511,9 @@ def installed_distribution(built_distribution, request): def test_wheel_and_sdist_contain_exact_complete_sql_chain(built_distribution): - _, wheel, source = built_distribution + """The DDL chain and catalog manifests ship in the storage distribution, not adcp.""" + _, adcp_wheel, _ = built_distribution + wheel, source = storage_distribution(adcp_wheel), storage_source(adcp_wheel) with zipfile.ZipFile(wheel) as archive, tarfile.open(source) as tar: prefix = tar.getnames()[0].split("/")[0] for name in ( @@ -449,22 +522,22 @@ def test_wheel_and_sdist_contain_exact_complete_sql_chain(built_distribution): "reporting_status_notifications.sql", "reporting_status_selector_version.sql", ): - expected = (ROOT / "src" / "adcp" / "reporting" / "ledger" / name).read_bytes() - assert archive.read(f"adcp/reporting/ledger/{name}") == expected - member = tar.extractfile(f"{prefix}/src/adcp/reporting/ledger/{name}") + expected = (STORAGE_SRC / "ledger" / name).read_bytes() + assert archive.read(f"adcp_reporting_storage/ledger/{name}") == expected + member = tar.extractfile(f"{prefix}/src/adcp_reporting_storage/ledger/{name}") assert member is not None and member.read() == expected assert ( - archive.read("adcp/reporting/outbox/_schema.py") - == (ROOT / "src" / "adcp" / "reporting" / "outbox" / "_schema.py").read_bytes() + archive.read("adcp_reporting_storage/outbox/_schema.py") + == (STORAGE_SRC / "outbox" / "_schema.py").read_bytes() ) - expected = (ROOT / "src/adcp/reporting/outbox/required_schema.json").read_bytes() - assert archive.read("adcp/reporting/outbox/required_schema.json") == expected - member = tar.extractfile(f"{prefix}/src/adcp/reporting/outbox/required_schema.json") - assert member is not None and member.read() == expected - expected = (ROOT / "src/adcp/reporting/outbox/required_status_schema.json").read_bytes() - assert archive.read("adcp/reporting/outbox/required_status_schema.json") == expected - member = tar.extractfile(f"{prefix}/src/adcp/reporting/outbox/required_status_schema.json") - assert member is not None and member.read() == expected + for name in ("required_schema.json", "required_status_schema.json"): + expected = (STORAGE_SRC / "outbox" / name).read_bytes() + assert archive.read(f"adcp_reporting_storage/outbox/{name}") == expected + member = tar.extractfile(f"{prefix}/src/adcp_reporting_storage/outbox/{name}") + assert member is not None and member.read() == expected + with zipfile.ZipFile(adcp_wheel) as archive: + shipped = [n for n in archive.namelist() if n.startswith("adcp/reporting/ledger/")] + assert not [n for n in shipped if n.endswith((".sql", ".json"))], shipped def test_installed_base_import_needs_no_pg_extra(installed_distribution): @@ -489,7 +562,11 @@ async def test_installed_pg_extra_migrates_commits_and_restarts(installed_distri "now": NOW.isoformat(), "rows": rows, "required_objects": json.loads( - (ROOT / "src/adcp/reporting/outbox/required_schema.json").read_text() + ( + ROOT + / "packages/adcp-reporting-storage/src/adcp_reporting_storage" + / "outbox/required_schema.json" + ).read_text() ), "waiver_objects": WAIVER_OBJECTS, "provisional_objects": PROVISIONAL_OBJECTS, @@ -508,23 +585,24 @@ async def test_installed_pg_extra_migrates_commits_and_restarts(installed_distri from pydantic import TypeAdapter from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( - PgReportingReconciliationStore, ReportingConfiguration, ReportingObligationRecord, ReportingRevisionRecord, ) from adcp.reporting.outbox import ( - ActivityOutcome, ActivityRequest, PgReportingOutbox, ReportingEnvelopeCipher, + ActivityOutcome, ActivityRequest, ReportingEnvelopeCipher, ReportingLegacyAuthentication, ReportingNotificationSubscription, ReportingNotificationWorker, ReportingActivityProjector, ReportingActivitySupport, - PgStatusNotificationStore, ) -from adcp.reporting.outbox._schema import schema_objects +from adcp_reporting_storage import ( + PgReportingOutbox, PgReportingReconciliationStore, PgStatusNotificationStore, +) +from adcp_reporting_storage.outbox._schema import schema_objects values = json.load(sys.stdin) assert len(values["waiver_objects"]) == 10 assert len(values["provisional_objects"]) == 27 -assert json.loads(files("adcp.reporting.ledger").joinpath( +assert json.loads(files("adcp_reporting_storage.ledger").joinpath( "required_provisional_schema.json").read_text()) == values["provisional_objects"] expected_objects = { **values["required_objects"], **values["waiver_objects"], **values["provisional_objects"] @@ -561,7 +639,7 @@ async def get_active(self, *, account_id, subscriber_id, notification_type): assert objects == expected_objects required = {key: objects[key] for key in values["required_objects"]} assert json.dumps(required, indent=2, sort_keys=True) + "\n" == files( - "adcp.reporting.outbox" + "adcp_reporting_storage.outbox" ).joinpath("required_schema.json").read_text() await store.put_configuration( TypeAdapter(ReportingConfiguration).validate_python(values["config"]) diff --git a/tests/conformance/reporting/test_reporting_notification_process_matrix.py b/tests/conformance/reporting/test_reporting_notification_process_matrix.py index c918bbe4f..bbdaa12f9 100644 --- a/tests/conformance/reporting/test_reporting_notification_process_matrix.py +++ b/tests/conformance/reporting/test_reporting_notification_process_matrix.py @@ -13,9 +13,9 @@ from functools import wraps import pytest +from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore -from adcp.reporting.ledger import PgReportingReconciliationStore -from adcp.reporting.outbox import DeliveryLease, PgReportingOutbox +from adcp.reporting.outbox import DeliveryLease from ._generation_support import isolated_reporting_pool from ._reliable_support import ManualClock, _BytesStore, service_process diff --git a/tests/conformance/reporting/test_reporting_notification_transactions.py b/tests/conformance/reporting/test_reporting_notification_transactions.py index 2c6fc70a5..176faeaee 100644 --- a/tests/conformance/reporting/test_reporting_notification_transactions.py +++ b/tests/conformance/reporting/test_reporting_notification_transactions.py @@ -8,17 +8,16 @@ from datetime import timedelta import pytest +from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, - PgReportingReconciliationStore, ReportingAdjustmentRecord, ReportingDeliveryScope, ReportingMaterializationCheck, ReportingObligationDeliveryRecord, derive_period, ) -from adcp.reporting.outbox import PgReportingOutbox from ._generation_support import END, NOW, START, configuration, obligation_for, revision_for from ._reconciliation_support import scenario diff --git a/tests/conformance/reporting/test_reporting_ownership_packaging.py b/tests/conformance/reporting/test_reporting_ownership_packaging.py index 1f5406722..85135ac17 100644 --- a/tests/conformance/reporting/test_reporting_ownership_packaging.py +++ b/tests/conformance/reporting/test_reporting_ownership_packaging.py @@ -13,7 +13,13 @@ from ._generation_support import require_rolling_database from .test_reporting_materializer_packaging import b1_wheels, built_distribution -from .test_reporting_notification_packaging import ROOT, run_step +from .test_reporting_notification_packaging import ( + ROOT, + STORAGE_SRC, + run_step, + storage_distribution, + storage_source, +) __all__ = ["b1_wheels", "built_distribution"] @@ -28,19 +34,20 @@ ReportingScheduleSpec, ReportingStatusCaller, ) -from adcp.reporting.migration import ReportingOwnershipBackfill +from adcp_reporting_storage import postgres_reliable_reporting_service +from adcp_reporting_storage.migration import ReportingOwnershipBackfill from adcp.reporting.service import ReliableReportingService from adcp.server import ADCPHandler from psycopg import AsyncConnection, sql from psycopg_pool import AsyncConnectionPool settings = json.load(sys.stdin) -assert Path(__import__("adcp.reporting.migration", fromlist=["*"]).__file__).is_relative_to( +assert Path(__import__("adcp_reporting_storage.migration", fromlist=["*"]).__file__).is_relative_to( Path(sys.prefix) ) assert ( "retain_without_replay" - in files("adcp.reporting.ledger").joinpath("reporting_caller_ownership.sql").read_text() + in files("adcp_reporting_storage.ledger").joinpath("reporting_caller_ownership.sql").read_text() ) @@ -100,7 +107,7 @@ async def main(): ) as pool: await pool.wait() await verify( - ReliableReportingService.postgres( + postgres_reliable_reporting_service( pool=pool, account_context=lambda _: None, caller_resolver=resolver ) ) @@ -145,13 +152,18 @@ async def test_owned_resources_and_private_reads_installed(request, kind): "outbox/required_schema.json", "materializer/required_schema.json", ] - with zipfile.ZipFile(wheels[kind]) as wheel, tarfile.open(source) as archive: + # Every one of these resources moved to the storage distribution (#1510). + with ( + zipfile.ZipFile(storage_distribution(wheels[kind])) as wheel, + tarfile.open(storage_source(source)) as archive, + ): prefix = archive.getnames()[0].split("/")[0] for relative in resources: - expected = (ROOT / "src/adcp/reporting" / relative).read_bytes() - assert wheel.read("adcp/reporting/" + relative) == expected + expected = (STORAGE_SRC / relative).read_bytes() + assert wheel.read("adcp_reporting_storage/" + relative) == expected assert ( - archive.extractfile(prefix + "/src/adcp/reporting/" + relative).read() == expected + archive.extractfile(prefix + "/src/adcp_reporting_storage/" + relative).read() + == expected ) environment = root / ("owned-installed-" + kind) await asyncio.to_thread( @@ -168,7 +180,14 @@ async def test_owned_resources_and_private_reads_installed(request, kind): ) await asyncio.to_thread( run_step, - [*installer, str(wheels[kind]) + "[pg]", "pytest", "pytest-asyncio", "asgi-lifespan"], + [ + *installer, + str(wheels[kind]) + "[pg]", + str(storage_distribution(wheels[kind])), + "pytest", + "pytest-asyncio", + "asgi-lifespan", + ], label="owned-install", cwd=root, timeout=180, diff --git a/tests/conformance/reporting/test_reporting_period_retirement_pg.py b/tests/conformance/reporting/test_reporting_period_retirement_pg.py index 814228a9b..543d528ca 100644 --- a/tests/conformance/reporting/test_reporting_period_retirement_pg.py +++ b/tests/conformance/reporting/test_reporting_period_retirement_pg.py @@ -6,9 +6,10 @@ from typing import Any import pytest +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.change_feed import encode_reporting_change_cursor_v1 -from adcp.reporting.ledger import PgReportingLedgerStore, ReportingRowStoreError -from adcp.reporting.ledger.change_feed import encode_reporting_change_cursor_v1 +from adcp.reporting.ledger import ReportingRowStoreError from adcp.reporting.ledger.store import LedgerConflictError from ._generation_support import configuration, isolated_reporting_pool, obligation_for @@ -242,8 +243,8 @@ async def test_a_retiring_period_refuses_new_revisions(tmp_path: Path) -> None: async def test_a_period_that_stopped_qualifying_is_not_retired(tmp_path: Path) -> None: - from adcp.reporting.ledger.period_retirement import _retire_period - from adcp.reporting.ledger.row_retention import NoLongerEligibleError + from adcp_reporting_storage.ledger.period_retirement import _retire_period + from adcp_reporting_storage.ledger.row_retention import NoLongerEligibleError async with isolated_reporting_pool() as pool: store = PgReportingLedgerStore(pool=pool, row_storage=options(tmp_path), change_feed=True) diff --git a/tests/conformance/reporting/test_reporting_production_lease_generations.py b/tests/conformance/reporting/test_reporting_production_lease_generations.py index 801f7fbee..f96502532 100644 --- a/tests/conformance/reporting/test_reporting_production_lease_generations.py +++ b/tests/conformance/reporting/test_reporting_production_lease_generations.py @@ -4,10 +4,11 @@ from datetime import datetime, timedelta, timezone import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgReportingMaterializerStore +from adcp_reporting_storage.materializer.schema import validate_materializer_schema -from adcp.reporting.ledger import LedgerConflictError, PgReportingLedgerStore -from adcp.reporting.materializer import PgReportingMaterializerStore, ReportingWriterError -from adcp.reporting.materializer.schema import validate_materializer_schema +from adcp.reporting.ledger import LedgerConflictError +from adcp.reporting.materializer import ReportingWriterError from ._durable_materializer_support import durable_case, durable_harness from ._generation_support import NOW, configuration, isolated_reporting_pool diff --git a/tests/conformance/reporting/test_reporting_production_lease_order.py b/tests/conformance/reporting/test_reporting_production_lease_order.py index 4adbbcd03..f36e7b58a 100644 --- a/tests/conformance/reporting/test_reporting_production_lease_order.py +++ b/tests/conformance/reporting/test_reporting_production_lease_order.py @@ -5,13 +5,16 @@ from dataclasses import replace import pytest +from adcp_reporting_storage import ( + PgReportingFeedStore, + PgReportingLedgerStore, + PgReportingMaterializerStore, + PgReportingProjectionStore, + PgReportingReceiptStore, + PgReportingReconciliationStore, +) -from adcp.reporting.feed import PgReportingFeedStore -from adcp.reporting.ledger import PgReportingLedgerStore, PgReportingReconciliationStore from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.materializer import PgReportingMaterializerStore -from adcp.reporting.projection import PgReportingProjectionStore -from adcp.reporting.receipts import PgReportingReceiptStore from ._generation_support import ( NOW, diff --git a/tests/conformance/reporting/test_reporting_production_lock_retry.py b/tests/conformance/reporting/test_reporting_production_lock_retry.py index 4ad751983..d2084f37d 100644 --- a/tests/conformance/reporting/test_reporting_production_lock_retry.py +++ b/tests/conformance/reporting/test_reporting_production_lock_retry.py @@ -4,15 +4,10 @@ from contextlib import asynccontextmanager import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgReportingMaterializerStore -from adcp.reporting.ledger import ( - LedgerConflictError, - PgReportingLedgerStore, - ProducerOfferings, - ReportingProducer, -) +from adcp.reporting.ledger import LedgerConflictError, ProducerOfferings, ReportingProducer from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.materializer import PgReportingMaterializerStore from ._generation_support import START, UncalledSource, configuration, isolated_reporting_pool from ._production_support import production_harness diff --git a/tests/conformance/reporting/test_reporting_production_migration.py b/tests/conformance/reporting/test_reporting_production_migration.py index 5c06050c8..8e32a102b 100644 --- a/tests/conformance/reporting/test_reporting_production_migration.py +++ b/tests/conformance/reporting/test_reporting_production_migration.py @@ -7,15 +7,15 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingFeedStore +from adcp_reporting_storage.outbox._schema import schema_objects +from adcp_reporting_storage.outbox.status_pg import PgStatusNotificationStore +from adcp_reporting_storage.production.pg import PgReportingProductionStore +from adcp_reporting_storage.production.schema import validate_production_schema +from adcp_reporting_storage.projection.schema import validate_projection_schema -from adcp.reporting.feed import PgReportingFeedStore from adcp.reporting.ledger.notification_models import ReportingNotificationError from adcp.reporting.materializer import ReportingMaterializerLease -from adcp.reporting.outbox._schema import schema_objects -from adcp.reporting.outbox.status_pg import PgStatusNotificationStore -from adcp.reporting.production.pg import PgReportingProductionStore -from adcp.reporting.production.schema import validate_production_schema -from adcp.reporting.projection.schema import validate_projection_schema from ._durable_materializer_support import DurableHarness, durable_case from ._feed_support import feed_request, mixed_case, walk @@ -27,13 +27,15 @@ def manifests(): required = { package: json.loads( - files("adcp.reporting." + package).joinpath("required_schema.json").read_text() + files("adcp_reporting_storage." + package).joinpath("required_schema.json").read_text() ) for package in ("materializer", "receipts", "feed", "projection", "production") } required["production"].update( json.loads( - files("adcp.reporting.production").joinpath("service_context_schema.json").read_text() + files("adcp_reporting_storage.production") + .joinpath("service_context_schema.json") + .read_text() ) ) return required diff --git a/tests/conformance/reporting/test_reporting_production_notifications.py b/tests/conformance/reporting/test_reporting_production_notifications.py index c0792364a..725ecfd35 100644 --- a/tests/conformance/reporting/test_reporting_production_notifications.py +++ b/tests/conformance/reporting/test_reporting_production_notifications.py @@ -179,7 +179,7 @@ def use_clock(h, moment): async def pin_current_clock(h): moment = h.clock() if h.pool is not None: - from adcp.reporting.outbox.pg import database_now + from adcp_reporting_storage.outbox.pg import database_now async with h.pool.connection() as connection: moment = await database_now(connection, None) diff --git a/tests/conformance/reporting/test_reporting_production_packaging.py b/tests/conformance/reporting/test_reporting_production_packaging.py index e31497061..a3cb414d1 100644 --- a/tests/conformance/reporting/test_reporting_production_packaging.py +++ b/tests/conformance/reporting/test_reporting_production_packaging.py @@ -10,7 +10,7 @@ from ._production_installed import assert_installed_schema_keys from ._production_packaging import installed_production, production_schema_hashes from .test_reporting_materializer_packaging import b1_wheels, built_distribution -from .test_reporting_notification_packaging import run_step +from .test_reporting_notification_packaging import run_step, storage_distribution __all__ = ["b1_wheels", "built_distribution"] @@ -54,7 +54,11 @@ async def test_floor_installed_production_contract(request, kind, drivers): ) await asyncio.to_thread( run_step, - [*installer, str(wheels[kind]) + ("[pg]" if drivers else "")], + [ + *installer, + str(wheels[kind]) + ("[pg]" if drivers else ""), + str(storage_distribution(wheels[kind])), + ], label=label + "-install", cwd=root, timeout=180, diff --git a/tests/conformance/reporting/test_reporting_production_readiness.py b/tests/conformance/reporting/test_reporting_production_readiness.py index bfc39a3b5..b71721451 100644 --- a/tests/conformance/reporting/test_reporting_production_readiness.py +++ b/tests/conformance/reporting/test_reporting_production_readiness.py @@ -37,7 +37,7 @@ async def test_warm_proof_rechecks_the_actual_mount(transport, mutation, tmp_pat async def test_shared_scan_cancellation_and_post_scan_dynamic_check(tmp_path, monkeypatch): - import adcp.reporting.production.schema as schema + import adcp_reporting_storage.production.schema as schema async with production_harness("postgres", tmp_path / "destination.sqlite") as h: support = h.production @@ -83,7 +83,7 @@ async def test_discovery_preserves_declaration_while_admission_rechecks_pool( ): from contextlib import AsyncExitStack - import adcp.reporting.production.schema as schema + import adcp_reporting_storage.production.schema as schema pool_type = pytest.importorskip("psycopg_pool").AsyncConnectionPool async with production_harness( @@ -160,7 +160,7 @@ async def test_discovery_preserves_declaration_while_admission_rechecks_queue_po ): from contextlib import AsyncExitStack - import adcp.reporting.production.schema as schema + import adcp_reporting_storage.production.schema as schema pool_type = pytest.importorskip("psycopg_pool").AsyncConnectionPool # The projection queue also participates without optional HTTP workers. @@ -223,7 +223,7 @@ async def no_scan(*args, **kwargs): async def test_warm_mounted_discovery_does_not_checkout_a_saturated_valid_pool( notifications, tmp_path, monkeypatch ): - import adcp.reporting.production.schema as schema + import adcp_reporting_storage.production.schema as schema async with production_harness( "postgres", @@ -314,7 +314,7 @@ async def test_optional_delivery_is_owned_and_broken_enabled_chain_fails_closed( async def test_discovery_preserves_signing_declaration_while_admission_rechecks_wiring( tmp_path, monkeypatch ): - import adcp.reporting.production.schema as schema + import adcp_reporting_storage.production.schema as schema async with production_harness( "postgres", tmp_path / "destination.sqlite", notifications=True, notification_delivery=True diff --git a/tests/conformance/reporting/test_reporting_production_rolling.py b/tests/conformance/reporting/test_reporting_production_rolling.py index 01f6d6f5b..f09bb6694 100644 --- a/tests/conformance/reporting/test_reporting_production_rolling.py +++ b/tests/conformance/reporting/test_reporting_production_rolling.py @@ -58,7 +58,9 @@ def production_parent(request, tmp_path_factory): with zipfile.ZipFile(wheel) as archive: modules = {} - for name in {*feed_modules(), "adcp.reporting.outbox.status_pg"}: + for name in {*feed_modules(), "adcp_reporting_storage.outbox.status_pg"}: + # The hardening parent predates the storage split: its stores live in adcp. + name = name.replace("adcp_reporting_storage.", "adcp.reporting.", 1) member = name.replace(".", "/") + ".py" if member not in archive.namelist(): member = name.replace(".", "/") + "/__init__.py" diff --git a/tests/conformance/reporting/test_reporting_production_service_factory.py b/tests/conformance/reporting/test_reporting_production_service_factory.py index 8d32220f6..822412377 100644 --- a/tests/conformance/reporting/test_reporting_production_service_factory.py +++ b/tests/conformance/reporting/test_reporting_production_service_factory.py @@ -1,3 +1,5 @@ +from adcp_reporting_storage import postgres_reliable_reporting_service + """Adapter registration builds a durable source and the actual production graph.""" import asyncio @@ -8,6 +10,7 @@ from types import SimpleNamespace import pytest +from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore from adcp.decisioning.capabilities import Account as AccountCapabilities from adcp.reporting import ( @@ -20,7 +23,6 @@ ) from adcp.reporting.fixtures import redacted_capabilities, redacted_snapshot_request from adcp.reporting.inline_source import InMemorySealStore, InMemoryStagingStore -from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore from adcp.reporting.ledger import ProducerOfferings, ReportingDestinationBinding from adcp.reporting.materializer import ReportingRevisionVerifierRegistry from adcp.reporting.materializer.contracts import ReportingWriterCapability, ReportingWriterError @@ -53,7 +55,7 @@ async def test_postgres_factory_replays_source_after_restart_without_refetch(aut async with isolated_reporting_pool(autocommit=autocommit) as pool: request = redacted_snapshot_request() clock = DeterministicReportingClock(request.period.source_read_cutoff_at) - first = ReliableReportingService.postgres( + first = postgres_reliable_reporting_service( pool=pool, account_context=_account_context, clock=clock ) adapter = ScriptedReportingAdapter(redacted_capabilities(), [_rows(9)]) @@ -67,7 +69,7 @@ async def test_postgres_factory_replays_source_after_restart_without_refetch(aut finally: await first.close() - restarted = ReliableReportingService.postgres( + restarted = postgres_reliable_reporting_service( pool=pool, account_context=_account_context, clock=clock ) empty = ScriptedReportingAdapter(redacted_capabilities()) @@ -100,7 +102,7 @@ async def test_postgres_factory_replays_source_after_restart_without_refetch(aut async def test_postgres_factory_preserves_explicit_source_store_overrides(): async with isolated_reporting_pool() as pool: - service = ReliableReportingService.postgres(pool=pool, account_context=_account_context) + service = postgres_reliable_reporting_service(pool=pool, account_context=_account_context) staging, seals = InMemoryStagingStore(), InMemorySealStore() registered = service.sources.register( "gam", ScriptedReportingAdapter(redacted_capabilities()), staging=staging, seals=seals @@ -297,7 +299,7 @@ async def authorize(account, context, consumer): **notification_inputs, ) factory = ( - ReliableReportingService.memory if pool is None else ReliableReportingService.postgres + ReliableReportingService.memory if pool is None else postgres_reliable_reporting_service ) h.service = factory( account_context=context, diff --git a/tests/conformance/reporting/test_reporting_production_testing_kit.py b/tests/conformance/reporting/test_reporting_production_testing_kit.py index 43a0a05b2..5eead694b 100644 --- a/tests/conformance/reporting/test_reporting_production_testing_kit.py +++ b/tests/conformance/reporting/test_reporting_production_testing_kit.py @@ -71,8 +71,11 @@ async def backend(request: pytest.FixtureRequest) -> AsyncIterator[Backend]: ) yield Backend(clock, lambda: retained) else: - from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.inline_storage import ( + PgReportingSealStore, + PgReportingStagingStore, + ) + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore async with isolated_reporting_pool(autocommit=request.param == "pg-autocommit") as pool: async with pool.connection() as connection: diff --git a/tests/conformance/reporting/test_reporting_production_transactions.py b/tests/conformance/reporting/test_reporting_production_transactions.py index ab07902e8..1a6d814fc 100644 --- a/tests/conformance/reporting/test_reporting_production_transactions.py +++ b/tests/conformance/reporting/test_reporting_production_transactions.py @@ -153,7 +153,7 @@ async def finish(): else: with monkeypatch.context() as patch: if h.pool is not None: - import adcp.reporting.materializer.pg as pg + import adcp_reporting_storage.materializer.pg as pg async def empty(connection, event): pass diff --git a/tests/conformance/reporting/test_reporting_projection_history.py b/tests/conformance/reporting/test_reporting_projection_history.py index 6a69890ad..4b6fa3dfa 100644 --- a/tests/conformance/reporting/test_reporting_projection_history.py +++ b/tests/conformance/reporting/test_reporting_projection_history.py @@ -139,7 +139,7 @@ async def history_replay(h, monkeypatch, *, legacy_baseline): old = InMemoryStatusNotificationStore(h.store) else: - from adcp.reporting.outbox.status_pg import PgStatusNotificationStore + from adcp_reporting_storage.outbox.status_pg import PgStatusNotificationStore old = PgStatusNotificationStore(h.store) production_operation_5 = await old.baseline(account_id=account) diff --git a/tests/conformance/reporting/test_reporting_projection_notifications.py b/tests/conformance/reporting/test_reporting_projection_notifications.py index 83b2604f4..f1cb5dbd7 100644 --- a/tests/conformance/reporting/test_reporting_projection_notifications.py +++ b/tests/conformance/reporting/test_reporting_projection_notifications.py @@ -17,7 +17,7 @@ async def test_status_queue_isolation_and_atomic_projection(backend, fault, monk if h.pool is None: old = InMemoryReportingStatusOutbox(h.store) else: - from adcp.reporting.outbox.status_pg import PgReportingStatusOutbox + from adcp_reporting_storage.outbox.status_pg import PgReportingStatusOutbox old = PgReportingStatusOutbox(pool=h.pool) assert await old.list_events(account_id=account) == () diff --git a/tests/conformance/reporting/test_reporting_projection_rc6.py b/tests/conformance/reporting/test_reporting_projection_rc6.py index b698b077a..26ad999d7 100644 --- a/tests/conformance/reporting/test_reporting_projection_rc6.py +++ b/tests/conformance/reporting/test_reporting_projection_rc6.py @@ -131,7 +131,7 @@ def request(version, view="summary"): def capture_clock(h, monkeypatch): if h.pool is not None: - from adcp.reporting.feed import pg + from adcp_reporting_storage.feed import pg async def now(connection): # Same account-locked SQL read with an explicit conformance instant. @@ -283,7 +283,7 @@ async def test_frozen_continuations_retain_bytes_and_reject_cross_version_positi if key not in {"_clock", "_lock"}: vars(store)[key] = deepcopy(value) else: - from adcp.reporting.projection.pg import PgReportingProjectionStore + from adcp_reporting_storage.projection.pg import PgReportingProjectionStore store = PgReportingProjectionStore(pool=h.pool, clock=h.clock) h.store = store diff --git a/tests/conformance/reporting/test_reporting_provisional_catalog.py b/tests/conformance/reporting/test_reporting_provisional_catalog.py index 6c2da83bc..c1e97a845 100644 --- a/tests/conformance/reporting/test_reporting_provisional_catalog.py +++ b/tests/conformance/reporting/test_reporting_provisional_catalog.py @@ -5,11 +5,11 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgReportingReceiptStore +from adcp_reporting_storage.outbox._schema import REQUIRED_OBJECTS, schema_objects +from adcp_reporting_storage.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.outbox._schema import REQUIRED_OBJECTS, schema_objects -from adcp.reporting.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.receipts import PgReportingReceiptStore, ReportingReceiptError +from adcp.reporting.receipts import ReportingReceiptError from ._durable_materializer_support import DurableHarness, durable_case from ._generation_support import ( @@ -29,7 +29,7 @@ async def test_atomic_observation_bootstrap_preserves_exact_767_parent_objects( ): # This is a source-level DDL control, not installed historical-binary proof. # All eight pre-observation migration files and old manifests stay unchanged. - root = files("adcp.reporting.ledger") + root = files("adcp_reporting_storage.ledger") chain = ( "reporting_ledger.sql", "reporting_ledger_account_generations.sql", @@ -51,7 +51,9 @@ async def test_atomic_observation_bootstrap_preserves_exact_767_parent_objects( for package in ("materializer", "receipts"): expected_parent.update( json.loads( - files("adcp.reporting." + package).joinpath("required_schema.json").read_text() + files("adcp_reporting_storage." + package) + .joinpath("required_schema.json") + .read_text() ) ) assert len(expected_parent) == 767 diff --git a/tests/conformance/reporting/test_reporting_provisional_catalog_scope.py b/tests/conformance/reporting/test_reporting_provisional_catalog_scope.py index 61025154b..5dde7569d 100644 --- a/tests/conformance/reporting/test_reporting_provisional_catalog_scope.py +++ b/tests/conformance/reporting/test_reporting_provisional_catalog_scope.py @@ -4,17 +4,17 @@ from importlib.resources import files import pytest - -from adcp.reporting.ledger import LedgerConflictError -from adcp.reporting.outbox._schema import ( +from adcp_reporting_storage import PgReportingReceiptStore +from adcp_reporting_storage.outbox._schema import ( PROVISIONAL_REQUIRED_OBJECTS, REQUIRED_OBJECTS, _catalog_objects, schema_objects, validate_provisional_schema, ) -from adcp.reporting.outbox.status_schema import REQUIRED_STATUS_OBJECTS -from adcp.reporting.receipts import PgReportingReceiptStore +from adcp_reporting_storage.outbox.status_schema import REQUIRED_STATUS_OBJECTS + +from adcp.reporting.ledger import LedgerConflictError from ._generation_support import isolated_reporting_pool @@ -63,7 +63,9 @@ async def test_exact_catalog_scope_preserves_full_contract_and_excludes_overload for package in ("materializer", "receipts"): expected.update( json.loads( - files("adcp.reporting." + package).joinpath("required_schema.json").read_text() + files("adcp_reporting_storage." + package) + .joinpath("required_schema.json") + .read_text() ) ) assert len(expected) == 794 and len(PROVISIONAL_REQUIRED_OBJECTS) == 27 diff --git a/tests/conformance/reporting/test_reporting_provisional_readiness.py b/tests/conformance/reporting/test_reporting_provisional_readiness.py index 5543027e7..47b8ad476 100644 --- a/tests/conformance/reporting/test_reporting_provisional_readiness.py +++ b/tests/conformance/reporting/test_reporting_provisional_readiness.py @@ -1,8 +1,9 @@ """Provisional persistence cannot operate with a partial or unguarded catalog.""" import pytest +from adcp_reporting_storage import PgReportingLedgerStore -from adcp.reporting.ledger import LedgerConflictError, PgReportingLedgerStore +from adcp.reporting.ledger import LedgerConflictError from ._generation_support import isolated_reporting_pool @@ -75,8 +76,9 @@ async def test_provisional_operations_require_the_complete_immutable_extension( @pytest.mark.parametrize("damage", DAMAGE) async def test_notification_readiness_checks_a_present_known_extension(damage): + from adcp_reporting_storage.outbox._schema import validate_schema + from adcp.reporting.outbox import ReportingNotificationError - from adcp.reporting.outbox._schema import validate_schema async with isolated_reporting_pool(autocommit=True) as pool: await PgReportingLedgerStore(pool=pool).create_schema() diff --git a/tests/conformance/reporting/test_reporting_publication_time.py b/tests/conformance/reporting/test_reporting_publication_time.py index c478c2cfd..96bbba85b 100644 --- a/tests/conformance/reporting/test_reporting_publication_time.py +++ b/tests/conformance/reporting/test_reporting_publication_time.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta, timezone import pytest +from adcp_reporting_storage import PgReportingLedgerStore from pydantic import ValidationError from adcp.reporting import ( @@ -18,7 +19,6 @@ from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, LedgerConflictError, - PgReportingLedgerStore, ProducerOfferings, ReportingConfiguration, ReportingProducer, diff --git a/tests/conformance/reporting/test_reporting_receipt_diagnostics.py b/tests/conformance/reporting/test_reporting_receipt_diagnostics.py index 392f661fd..68654df71 100644 --- a/tests/conformance/reporting/test_reporting_receipt_diagnostics.py +++ b/tests/conformance/reporting/test_reporting_receipt_diagnostics.py @@ -1,5 +1,17 @@ """Unexpected original failures are useful to operators without retaining payloads.""" +import importlib.util as _importlib_util + + +def _storage_split(): + """True when the adcp under test predates the storage split (#1510). + + The installed hardening controls run this module inside historical wheels, + whose stores still live in adcp.reporting. + """ + return _importlib_util.find_spec("adcp.reporting.ledger.pg") is not None + + import asyncio import json import logging @@ -540,10 +552,13 @@ async def test_raw_store_boundary_label_assertion_is_load_bearing(caplog, monkey """ caplog.set_level(logging.ERROR) async with receipt_harness("postgres") as h: + if _storage_split(): # wheels before the storage split (#1510) + from adcp.reporting.receipts import pg as receipt_pg + else: + from adcp_reporting_storage.receipts import pg as receipt_pg from psycopg import AsyncConnection, OperationalError from adcp.reporting.receipts import _diagnostics - from adcp.reporting.receipts import pg as receipt_pg s, request = await sensitive_case(h) fired = [] diff --git a/tests/conformance/reporting/test_reporting_receipt_graph.py b/tests/conformance/reporting/test_reporting_receipt_graph.py index 92637ac71..27ad1af7b 100644 --- a/tests/conformance/reporting/test_reporting_receipt_graph.py +++ b/tests/conformance/reporting/test_reporting_receipt_graph.py @@ -300,13 +300,14 @@ async def test_complete_receipt_chain_rejects_damage_with_one_apparent_leaf(faul "fault", ["fork", "cycle", "gap", "cross_target", "accepted_predecessor", "disconnected_cycle"] ) async def test_literal_sql_blocks_forks_and_detects_damaged_existing_chains(fault): + from adcp_reporting_storage.ledger.delivery_pg import _IDENTITY_COLUMNS + from adcp.reporting.ledger._delivery_state import ( change_id, fingerprint, payload, storage_identity, ) - from adcp.reporting.ledger.delivery_pg import _IDENTITY_COLUMNS from ._generation_support import NOW @@ -561,13 +562,14 @@ async def test_complete_adjustment_receipt_chain_rejects_damage(fault): async def test_literal_sql_blocks_damaged_adjustment_chains(fault): from contextlib import nullcontext + from adcp_reporting_storage.ledger.delivery_pg import _IDENTITY_COLUMNS + from adcp.reporting.ledger._delivery_state import ( change_id, fingerprint, payload, storage_identity, ) - from adcp.reporting.ledger.delivery_pg import _IDENTITY_COLUMNS from ._generation_support import NOW diff --git a/tests/conformance/reporting/test_reporting_receipt_installed_pg.py b/tests/conformance/reporting/test_reporting_receipt_installed_pg.py index 3d2488963..4c47733e7 100644 --- a/tests/conformance/reporting/test_reporting_receipt_installed_pg.py +++ b/tests/conformance/reporting/test_reporting_receipt_installed_pg.py @@ -7,9 +7,9 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingReceiptStore from adcp.reporting.ledger.delivery import receipt_to_wire -from adcp.reporting.receipts import PgReportingReceiptStore from ._durable_materializer_support import DurableHarness from ._generation_support import isolated_reporting_pool @@ -17,6 +17,7 @@ from ._reconciliation_support import Clock from .test_reporting_materializer_installed_pg import installed_materializer from .test_reporting_materializer_packaging import ROOT, b1_wheels, built_distribution, run_step +from .test_reporting_notification_packaging import STORAGE_SRC from .test_reporting_receipt_process import worker __all__ = ["installed_materializer", "b1_wheels", "built_distribution"] @@ -37,8 +38,10 @@ def installed_receipts(installed_materializer): timeout=60, ) modules = dict(original["modules"]) + modules["adcp_reporting_storage.receipts.pg"] = hashlib.sha256( + (STORAGE_SRC / "receipts/pg.py").read_bytes() + ).hexdigest() for name in ( - "reporting/receipts/pg.py", "reporting/receipts/handler.py", "reporting/receipts/wire.py", "reporting/receipts/transport.py", diff --git a/tests/conformance/reporting/test_reporting_receipt_migration.py b/tests/conformance/reporting/test_reporting_receipt_migration.py index 71955facc..020e0bd61 100644 --- a/tests/conformance/reporting/test_reporting_receipt_migration.py +++ b/tests/conformance/reporting/test_reporting_receipt_migration.py @@ -5,20 +5,22 @@ from importlib.resources import files import pytest +from adcp_reporting_storage import PgReportingMaterializerStore, PgReportingReceiptStore +from adcp_reporting_storage.outbox._schema import schema_objects -from adcp.reporting.materializer import PgReportingMaterializerStore -from adcp.reporting.outbox._schema import schema_objects -from adcp.reporting.receipts import PgReportingReceiptStore, ReportingReceiptError +from adcp.reporting.receipts import ReportingReceiptError from ._durable_materializer_support import DurableHarness, durable_case from ._generation_support import isolated_reporting_pool from ._receipt_support import receipt_case, receipt_harness, request_for from ._reconciliation_support import Clock -SQL = files("adcp.reporting.ledger").joinpath("reporting_receipt_ingestion.sql").read_text() -MANIFEST = json.loads(files("adcp.reporting.receipts").joinpath("required_schema.json").read_text()) +SQL = files("adcp_reporting_storage.ledger").joinpath("reporting_receipt_ingestion.sql").read_text() +MANIFEST = json.loads( + files("adcp_reporting_storage.receipts").joinpath("required_schema.json").read_text() +) PARENT_MANIFEST = json.loads( - files("adcp.reporting.materializer").joinpath("required_schema.json").read_text() + files("adcp_reporting_storage.materializer").joinpath("required_schema.json").read_text() ) diff --git a/tests/conformance/reporting/test_reporting_receipt_transports.py b/tests/conformance/reporting/test_reporting_receipt_transports.py index 90f0cd144..f9f78d6c5 100644 --- a/tests/conformance/reporting/test_reporting_receipt_transports.py +++ b/tests/conformance/reporting/test_reporting_receipt_transports.py @@ -389,7 +389,7 @@ async def test_a2a_to_mcp_replay_after_mount_restart_keeps_every_original_ordina _, first = await mount.a2a(client, request, v1=v1) assert [r["result"] for r in first["results"]] == ["failed", "recorded", "recorded"], first if h.pool is not None: - from adcp.reporting.receipts import PgReportingReceiptStore + from adcp_reporting_storage import PgReportingReceiptStore h.store = PgReportingReceiptStore(pool=h.pool, notifications=h.store._notifications_enabled) restarted = MountedReceipts(h, hydrated=True) diff --git a/tests/conformance/reporting/test_reporting_reconciliation_boundaries.py b/tests/conformance/reporting/test_reporting_reconciliation_boundaries.py index 0db00b36a..ffd254a1c 100644 --- a/tests/conformance/reporting/test_reporting_reconciliation_boundaries.py +++ b/tests/conformance/reporting/test_reporting_reconciliation_boundaries.py @@ -559,7 +559,7 @@ def find_spec(self, fullname, path=None, target=None): ) ReportingStatusHandler(InMemoryReportingLedgerStore()) ReportingStatusHandler(InMemoryReportingReconciliationStore()) -assert 'adcp.reporting.ledger.delivery_pg' not in sys.modules +assert 'adcp_reporting_storage.ledger.delivery_pg' not in sys.modules """ result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True) assert result.returncode == 0, result.stderr diff --git a/tests/conformance/reporting/test_reporting_reconciliation_changes.py b/tests/conformance/reporting/test_reporting_reconciliation_changes.py index ebcc95e53..cb1a2ba64 100644 --- a/tests/conformance/reporting/test_reporting_reconciliation_changes.py +++ b/tests/conformance/reporting/test_reporting_reconciliation_changes.py @@ -6,10 +6,10 @@ from typing import get_args import pytest +from adcp_reporting_storage import PgReportingReconciliationStore from adcp.reporting.ledger import ( LedgerConflictError, - PgReportingReconciliationStore, ReportingAdjustmentReceiptRecord, ReportingAdjustmentRecord, ReportingControlTotalRecord, diff --git a/tests/conformance/reporting/test_reporting_reconciliation_migration.py b/tests/conformance/reporting/test_reporting_reconciliation_migration.py index 1da11d95d..d72199e10 100644 --- a/tests/conformance/reporting/test_reporting_reconciliation_migration.py +++ b/tests/conformance/reporting/test_reporting_reconciliation_migration.py @@ -11,10 +11,10 @@ from typing import Any import pytest +from adcp_reporting_storage import PgReportingReconciliationStore from adcp.reporting.ledger import ( LedgerConflictError, - PgReportingReconciliationStore, ReportingDeliveryPrincipal, ReportingDeliveryRecord, ReportingMaterializationCheck, @@ -26,7 +26,7 @@ from .test_reporting_currency_migration import retained FIXTURES = Path(__file__).resolve().parents[2] / "fixtures" -RESOURCES = files("adcp.reporting.ledger") +RESOURCES = files("adcp_reporting_storage.ledger") MIGRATION = RESOURCES.joinpath("reporting_ledger_reconciliation.sql") @@ -43,10 +43,12 @@ async def test_upgrade_preserves_history_and_is_concurrent_idempotent( source: str, autocommit: bool ) -> None: require_rolling_database() + from adcp_reporting_storage.migration import ( + ReportingOwnershipMigrationError, + migrate_legacy_reporting, + ) from psycopg import sql - from adcp.reporting.migration import ReportingOwnershipMigrationError, migrate_legacy_reporting - async with isolated_reporting_pool(autocommit=autocommit) as pool: async with pool.connection() as connection: await connection.execute((FIXTURES / source).read_text()) @@ -86,7 +88,7 @@ async def test_upgrade_preserves_history_and_is_concurrent_idempotent( @pytest.mark.parametrize("autocommit", [False, True]) async def test_failed_extension_upgrade_rolls_back_prerequisites(autocommit: bool) -> None: pytest.importorskip("psycopg") - from adcp.reporting.migration import ReportingOwnershipMigrationError + from adcp_reporting_storage.migration import ReportingOwnershipMigrationError async with isolated_reporting_pool(autocommit=autocommit) as pool: async with pool.connection() as connection: @@ -336,10 +338,12 @@ async def initial_retained_state(pool, *, kind="revision_receipt", write_change= async def assert_retained_quarantine(pool, before): require_rolling_database() + from adcp_reporting_storage.migration import ( + ReportingOwnershipMigrationError, + migrate_legacy_reporting, + ) from psycopg import sql - from adcp.reporting.migration import ReportingOwnershipMigrationError, migrate_legacy_reporting - store = PgReportingReconciliationStore(pool=pool) with pytest.raises(ReportingOwnershipMigrationError): await store.create_schema() diff --git a/tests/conformance/reporting/test_reporting_reconciliation_sql.py b/tests/conformance/reporting/test_reporting_reconciliation_sql.py index 2f8996863..8d251bdaf 100644 --- a/tests/conformance/reporting/test_reporting_reconciliation_sql.py +++ b/tests/conformance/reporting/test_reporting_reconciliation_sql.py @@ -11,11 +11,12 @@ from typing import Any import pytest +from adcp_reporting_storage import PgReportingReconciliationStore +from adcp_reporting_storage.ledger.delivery_pg import _IDENTITY_COLUMNS from adcp.reporting.canonical_json import canonical_json_utf8_v1 from adcp.reporting.ledger import ( LedgerConflictError, - PgReportingReconciliationStore, ReportingAdjustmentReceiptRecord, ReportingAdjustmentRecord, ReportingControlTotalRecord, @@ -33,9 +34,8 @@ payload, storage_identity, ) -from adcp.reporting.ledger.delivery_pg import _IDENTITY_COLUMNS -RESOURCES = files("adcp.reporting.ledger") +RESOURCES = files("adcp_reporting_storage.ledger") from ._generation_support import END, NOW, configuration, isolated_reporting_pool from ._reconciliation_support import Scenario, scenario diff --git a/tests/conformance/reporting/test_reporting_row_migration_pg.py b/tests/conformance/reporting/test_reporting_row_migration_pg.py index 02d8f86ee..8c378a381 100644 --- a/tests/conformance/reporting/test_reporting_row_migration_pg.py +++ b/tests/conformance/reporting/test_reporting_row_migration_pg.py @@ -6,9 +6,9 @@ from typing import Any import pytest +from adcp_reporting_storage import PgReportingLedgerStore from adcp.reporting.ledger import ( - PgReportingLedgerStore, ReportingObligationRecord, ReportingRevisionRecord, ReportingRowStoreError, @@ -316,8 +316,9 @@ async def test_migration_requires_row_storage_and_a_valid_batch_size() -> None: async def test_the_installed_schema_stays_ready_after_the_legacy_table_is_retired() -> None: + from adcp_reporting_storage.outbox._schema import validate_schema + from adcp.reporting.ledger.notification_models import ReportingNotificationError - from adcp.reporting.outbox._schema import validate_schema async with isolated_reporting_pool() as pool: await seed_legacy(pool) diff --git a/tests/conformance/reporting/test_reporting_row_retention_pg.py b/tests/conformance/reporting/test_reporting_row_retention_pg.py index 6d8526ba4..3e494315f 100644 --- a/tests/conformance/reporting/test_reporting_row_retention_pg.py +++ b/tests/conformance/reporting/test_reporting_row_retention_pg.py @@ -7,14 +7,15 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore, ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.row_storage_object import ( ReportingRowObjectContextV1, ReportingRowObjectPutInputV1, create_filesystem_reporting_row_object_provider_v1, ) -from adcp.reporting.ledger.row_write import prepare_revision_row_set +from adcp_reporting_storage.ledger.row_write import prepare_revision_row_set + +from adcp.reporting.ledger import ReportingRowStoreError from adcp.reporting.ledger.store import LedgerConflictError from ._generation_support import isolated_reporting_pool diff --git a/tests/conformance/reporting/test_reporting_row_storage_alignment_pg.py b/tests/conformance/reporting/test_reporting_row_storage_alignment_pg.py index 9a40c3603..1dd2b8b86 100644 --- a/tests/conformance/reporting/test_reporting_row_storage_alignment_pg.py +++ b/tests/conformance/reporting/test_reporting_row_storage_alignment_pg.py @@ -12,12 +12,11 @@ from typing import Any import pytest - -from adcp.reporting.ledger.row_storage import ( +from adcp_reporting_storage.ledger.row_storage import ( REPORTING_ROW_STORAGE_DDL_PATH, REPORTING_ROW_STORAGE_TABLES, ) -from adcp.reporting.outbox._schema import _catalog_objects +from adcp_reporting_storage.outbox._schema import _catalog_objects from ._generation_support import isolated_reporting_pool diff --git a/tests/conformance/reporting/test_reporting_row_storage_interop_pg.py b/tests/conformance/reporting/test_reporting_row_storage_interop_pg.py index 232f168e7..9bf46fc8b 100644 --- a/tests/conformance/reporting/test_reporting_row_storage_interop_pg.py +++ b/tests/conformance/reporting/test_reporting_row_storage_interop_pg.py @@ -10,10 +10,13 @@ ADCP_JS_SDK_DIR=/path/to/adcp-client ADCP_PG_TEST_URL=postgresql://... \\ pytest tests/conformance/reporting/test_reporting_row_storage_interop_pg.py - Set ``ADCP_UPDATE_INTEROP_FIXTURES=1`` to refresh the committed fixtures from a live run. + CI runs it with ``ADCP_REQUIRE_JS_INTEROP=1``, so a missing build fails instead of skipping. + ``scripts/regenerate_row_storage_interop_fixtures.py`` runs the same steps to refresh the + committed fixtures. * The replay tests always run. They load the rows each SDK wrote, as recorded in ``tests/fixtures/reporting-row-storage-interop-v1/``, into a fresh database and bucket - and verify them through this SDK, so Python CI needs no Node. + and verify them through this SDK, so Python CI needs no Node. The fixtures are + committed, so a missing one is a repository defect, not an environment one. """ from __future__ import annotations @@ -26,9 +29,8 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 from . import _row_storage_interop as interop from ._generation_support import isolated_reporting_pool @@ -126,23 +128,11 @@ async def test_one_database_and_one_bucket_in_both_directions(tmp_path: Path) -> ("reports", "object", "filesystem"), ] - if os.environ.get("ADCP_UPDATE_INTEROP_FIXTURES") == "1": - interop.INTEROP_DIR.mkdir(parents=True, exist_ok=True) - for name, written in (("js-written", js_written), ("python-written", python_written)): - state = await interop.dump_state( - pool, root, [item["row_set_id"] for item in written] - ) - (interop.INTEROP_DIR / f"{name}.json").write_text( - json.dumps({"expected": written, "state": state}, indent=1, sort_keys=True) - + "\n" - ) - @pytest.mark.parametrize("writer", ["js-written", "python-written"]) async def test_rows_written_by_the_other_sdk_verify_here(writer: str, tmp_path: Path) -> None: path = interop.INTEROP_DIR / f"{writer}.json" - if not path.exists(): - pytest.skip("interop fixtures have not been generated") + assert path.is_file(), f"committed interop fixture is missing: {path}" recorded = json.loads(path.read_text()) root = tmp_path / "bucket" async with isolated_reporting_pool() as pool: @@ -158,8 +148,7 @@ async def test_the_recorded_object_bytes_decompress_to_the_golden_chunks(tmp_pat import gzip path = interop.INTEROP_DIR / "js-written.json" - if not path.exists(): - pytest.skip("interop fixtures have not been generated") + assert path.is_file(), f"committed interop fixture is missing: {path}" recorded = json.loads(path.read_text()) golden = { vector["id"]: [base64.b64decode(chunk) for chunk in vector["expected"]["chunks_base64"]] diff --git a/tests/conformance/reporting/test_reporting_row_storage_object_pg.py b/tests/conformance/reporting/test_reporting_row_storage_object_pg.py index f055d5955..aa2410045 100644 --- a/tests/conformance/reporting/test_reporting_row_storage_object_pg.py +++ b/tests/conformance/reporting/test_reporting_row_storage_object_pg.py @@ -7,19 +7,19 @@ from typing import Any import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore, ReportingRowStorageOptionsV1 -from adcp.reporting.ledger.row_storage import ( +from adcp_reporting_storage import PgReportingLedgerStore, ReportingRowStorageOptionsV1 +from adcp_reporting_storage.ledger.row_storage import ( ReportingObjectRowBindingV1, ReportingPostgresRowBindingV1, ) -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( ReportingRowObjectContextV1, ReportingRowObjectPutInputV1, create_filesystem_reporting_row_object_provider_v1, ) -from adcp.reporting.ledger.row_write import prepare_revision_row_set +from adcp_reporting_storage.ledger.row_write import prepare_revision_row_set + +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError from ._generation_support import isolated_reporting_pool from .test_reporting_row_storage_pg import fetch_all, many_rows, seeded, typed_revision, walk @@ -398,7 +398,7 @@ def test_bindings_are_validated_before_any_write(tmp_path: Path) -> None: providers = options(tmp_path).providers def make(**binding: Any) -> None: - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 defaults: dict[str, Any] = { "provider": "filesystem", @@ -430,7 +430,7 @@ def make(**binding: Any) -> None: def storage_with_bindings(bindings: Any) -> Any: - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 return ReportingRowStorageV1(ReportingRowStorageOptionsV1(bindings=bindings)) @@ -469,7 +469,7 @@ async def test_the_producer_stages_object_uploads_outside_its_publication_transa async def test_object_deletes_keep_referenced_objects_and_adopters_fail_closed( tmp_path: Path, ) -> None: - from adcp.reporting.ledger.row_storage import ReportingRowChunkLocatorV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowChunkLocatorV1 async with isolated_reporting_pool() as pool: store = PgReportingLedgerStore(pool=pool, row_storage=options(tmp_path)) @@ -556,7 +556,7 @@ async def probe(self, *args: Any, **kwargs: Any) -> None: await self.inner.probe(*args, **kwargs) async def put_if_absent(self, put: Any, context: Any) -> Any: - from adcp.reporting.ledger.row_storage_object import ReportingRowObjectPutResultV1 + from adcp_reporting_storage.ledger.row_storage_object import ReportingRowObjectPutResultV1 result = await self.inner.put_if_absent(put, context) return ReportingRowObjectPutResultV1( @@ -571,7 +571,7 @@ async def delete(self, delete: Any, context: Any) -> Any: return await self.inner.delete(delete, context) async def _stat(self, stat: Any, context: Any) -> Any: - from adcp.reporting.ledger.row_storage_object import ReportingRowObjectStatResultV1 + from adcp_reporting_storage.ledger.row_storage_object import ReportingRowObjectStatResultV1 self.calls.append("stat") found = await self.inner.stat(stat, context) @@ -788,8 +788,10 @@ async def test_rows_written_under_an_older_template_stay_readable_through_a_read ) -> None: from dataclasses import replace - from adcp.reporting.ledger import row_storage - from adcp.reporting.ledger.row_storage_object import assert_reporting_row_key_template_v1 + from adcp_reporting_storage.ledger import row_storage + from adcp_reporting_storage.ledger.row_storage_object import ( + assert_reporting_row_key_template_v1, + ) from .test_reporting_row_retention_pg import age diff --git a/tests/conformance/reporting/test_reporting_row_storage_pg.py b/tests/conformance/reporting/test_reporting_row_storage_pg.py index 9c2dc27a1..e8c7d4fce 100644 --- a/tests/conformance/reporting/test_reporting_row_storage_pg.py +++ b/tests/conformance/reporting/test_reporting_row_storage_pg.py @@ -12,19 +12,19 @@ from typing import Any import pytest +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.row_storage import ( + REPORTING_ROW_STORAGE_DDL_PATH, + ReportingRowStorageOptionsV1, + ReportingRowStoreError, +) from adcp.reporting.ledger import ( - PgReportingLedgerStore, ReportingControlTotalRecord, ReportingObligationRecord, ReportingRevisionRecord, revision_content_sha256, ) -from adcp.reporting.ledger.row_storage import ( - REPORTING_ROW_STORAGE_DDL_PATH, - ReportingRowStorageOptionsV1, - ReportingRowStoreError, -) from adcp.reporting.ledger.store import LedgerConflictError from ._generation_support import ( @@ -569,8 +569,8 @@ async def test_rows_with_fractional_and_exponent_numbers_round_trip_and_verify() async def test_the_packaged_catalog_manifest_matches_a_clean_migration() -> None: - from adcp.reporting.ledger.row_storage import REPORTING_ROW_STORAGE_TABLES - from adcp.reporting.outbox._schema import _catalog_objects + from adcp_reporting_storage.ledger.row_storage import REPORTING_ROW_STORAGE_TABLES + from adcp_reporting_storage.outbox._schema import _catalog_objects async with isolated_reporting_pool() as pool: async with pool.connection() as connection: @@ -584,7 +584,9 @@ async def test_the_packaged_catalog_manifest_matches_a_clean_migration() -> None from importlib.resources import files packaged = json.loads( - files("adcp.reporting.ledger").joinpath("required_row_storage_schema.json").read_text() + files("adcp_reporting_storage.ledger") + .joinpath("required_row_storage_schema.json") + .read_text() ) assert installed == packaged, "run scripts/generate_row_storage_catalog.py" diff --git a/tests/conformance/reporting/test_reporting_row_storage_readiness_pg.py b/tests/conformance/reporting/test_reporting_row_storage_readiness_pg.py index 41a7050a2..78b0ce3d8 100644 --- a/tests/conformance/reporting/test_reporting_row_storage_readiness_pg.py +++ b/tests/conformance/reporting/test_reporting_row_storage_readiness_pg.py @@ -3,9 +3,8 @@ from __future__ import annotations import pytest - -from adcp.reporting.ledger import PgReportingLedgerStore -from adcp.reporting.ledger.row_storage import ReportingRowStorageV1, ReportingRowStoreError +from adcp_reporting_storage import PgReportingLedgerStore +from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1, ReportingRowStoreError from ._generation_support import isolated_reporting_pool diff --git a/tests/conformance/reporting/test_reporting_source_authorization.py b/tests/conformance/reporting/test_reporting_source_authorization.py index 5d868d30c..b457a9b7d 100644 --- a/tests/conformance/reporting/test_reporting_source_authorization.py +++ b/tests/conformance/reporting/test_reporting_source_authorization.py @@ -151,7 +151,7 @@ async def test_remapping_during_publication_refuses_result_and_allows_retry( async with harness(backend, tmp_path / "destination.sqlite") as h: await h.production.activate(account_id=h.item.config.account_id) source = h.production.offerings[0].producer._source - publication = h.store._source_publication + publication = h.store.source_publication publications = 0 @asynccontextmanager @@ -163,7 +163,7 @@ async def remap_under_lock(account_id, **kwargs): source.bind_generation(h.item.config, product_id="catalog-5820") yield publish_seal - monkeypatch.setattr(h.store, "_source_publication", remap_under_lock) + monkeypatch.setattr(h.store, "source_publication", remap_under_lock) source.release.clear() running = asyncio.create_task(source_turn(h.production)) try: @@ -185,7 +185,7 @@ async def remap_under_lock(account_id, **kwargs): ) assert (seal is not None) == (boundary == "ledger_lock") - monkeypatch.setattr(h.store, "_source_publication", publication) + monkeypatch.setattr(h.store, "source_publication", publication) source.bind_generation(h.item.config, product_id="catalog-7391") source.rows = reference_rows(2) restored = await source_turn(h.production) @@ -245,7 +245,7 @@ async def test_seal_authorization_checked_after_obtaining_account_lock( source.release.clear() running = asyncio.create_task(source_turn(h.production)) waiting = asyncio.Event() - publication = h.store._source_publication + publication = h.store.source_publication @asynccontextmanager async def observe_publication(account_id, **kwargs): @@ -255,7 +255,7 @@ async def observe_publication(account_id, **kwargs): try: await asyncio.wait_for(source.started.wait(), 10) - monkeypatch.setattr(h.store, "_source_publication", observe_publication) + monkeypatch.setattr(h.store, "source_publication", observe_publication) async with publication(h.item.config.account_id): source.release.set() await asyncio.wait_for(waiting.wait(), 10) @@ -368,7 +368,7 @@ def account_context(config): if backend == "memory": store = InMemoryReportingLedgerStore(clock=lambda: END) else: - from adcp.reporting.ledger.pg import PgReportingLedgerStore + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore pool = await stack.enter_async_context(isolated_reporting_pool(autocommit=True)) store = PgReportingLedgerStore(pool=pool, clock=lambda: END) @@ -417,12 +417,10 @@ async def test_postgres_inline_publication_shares_size_one_pool( pools = pytest.importorskip("psycopg_pool") psycopg = pytest.importorskip("psycopg") + from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore + from adcp.reporting.inline_source import InlineReportingSource - from adcp.reporting.inline_storage import ( - InlineStorageError, - PgReportingSealStore, - PgReportingStagingStore, - ) + from adcp.reporting.inline_storage import InlineStorageError async with isolated_reporting_pool(autocommit=autocommit) as isolated: async with pools.AsyncConnectionPool( diff --git a/tests/conformance/reporting/test_reporting_status_boundaries.py b/tests/conformance/reporting/test_reporting_status_boundaries.py index 3db7336bf..20c376a3b 100644 --- a/tests/conformance/reporting/test_reporting_status_boundaries.py +++ b/tests/conformance/reporting/test_reporting_status_boundaries.py @@ -5,17 +5,12 @@ from datetime import timedelta import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgStatusNotificationStore +from adcp_reporting_storage.outbox.status_schema import validate_status_schema -from adcp.reporting.ledger import ( - PgReportingLedgerStore, - derive_period, -) from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.outbox import ( - PgStatusNotificationStore, - ReportingNotificationError, -) -from adcp.reporting.outbox.status_schema import validate_status_schema +from adcp.reporting.ledger import derive_period +from adcp.reporting.outbox import ReportingNotificationError from . import test_reporting_status_projection_contract as _contract from ._generation_support import ( @@ -241,7 +236,7 @@ async def blocked(): ], ) async def test_status_manifest_damage_fails_only_its_owned_feature(damage): - from adcp.reporting.outbox._schema import validate_schema + from adcp_reporting_storage.outbox._schema import validate_schema async with isolated_reporting_pool(autocommit=True) as pool: status = PgStatusNotificationStore(PgReportingLedgerStore(pool=pool, notifications=True)) diff --git a/tests/conformance/reporting/test_reporting_status_capabilities.py b/tests/conformance/reporting/test_reporting_status_capabilities.py index 1bffe0d75..93e07cdcc 100644 --- a/tests/conformance/reporting/test_reporting_status_capabilities.py +++ b/tests/conformance/reporting/test_reporting_status_capabilities.py @@ -5,13 +5,13 @@ from itertools import product import pytest +from adcp_reporting_storage import PgReportingActivityUnionStore from adcp.reporting.ledger import InMemoryReportingLedgerStore from adcp.reporting.ledger.status import ReportingStatusCaller, ReportingStatusHandler from adcp.reporting.ledger.status_projection import mismatch_key from adcp.reporting.ledger.status_server import ReportingStatusNotificationHandler from adcp.reporting.outbox import ( - PgReportingActivityUnionStore, ReportingActivityProjector, ReportingActivitySupport, ReportingNotificationError, diff --git a/tests/conformance/reporting/test_reporting_status_delivery.py b/tests/conformance/reporting/test_reporting_status_delivery.py index a58898e8b..5c6a98be3 100644 --- a/tests/conformance/reporting/test_reporting_status_delivery.py +++ b/tests/conformance/reporting/test_reporting_status_delivery.py @@ -8,12 +8,12 @@ from itertools import cycle import pytest +from adcp_reporting_storage import PgReportingActivityUnionStore from adcp.reporting.ledger import InMemoryReportingLedgerStore from adcp.reporting.ledger.status import ReportingStatusCaller, ReportingStatusHandler from adcp.reporting.ledger.status_server import ReportingStatusNotificationHandler from adcp.reporting.outbox import ( - PgReportingActivityUnionStore, ReportingActivityProjector, ReportingActivitySupport, ReportingNotificationError, diff --git a/tests/conformance/reporting/test_reporting_status_identity.py b/tests/conformance/reporting/test_reporting_status_identity.py index c5a0c3605..108ba38a6 100644 --- a/tests/conformance/reporting/test_reporting_status_identity.py +++ b/tests/conformance/reporting/test_reporting_status_identity.py @@ -5,14 +5,11 @@ from urllib.parse import quote import pytest +from adcp_reporting_storage import PgReportingReconciliationStore from adcp.decisioning.context import AuthInfo from adcp.decisioning.registry import BuyerAgent, HttpSigCredential -from adcp.reporting.ledger import ( - PgReportingReconciliationStore, - ReportingDeliveryPrincipal, - ReportingIssueLifecycle, -) +from adcp.reporting.ledger import ReportingDeliveryPrincipal, ReportingIssueLifecycle from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller from adcp.reporting.outbox import ( ReportingNotificationError, diff --git a/tests/conformance/reporting/test_reporting_status_migration.py b/tests/conformance/reporting/test_reporting_status_migration.py index b6b479653..0bc6b82cc 100644 --- a/tests/conformance/reporting/test_reporting_status_migration.py +++ b/tests/conformance/reporting/test_reporting_status_migration.py @@ -10,22 +10,22 @@ from pathlib import Path import pytest - -from adcp.reporting.ledger import PgReportingReconciliationStore -from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller -from adcp.reporting.ledger.status_projection import lifecycle_intents, mismatch_key -from adcp.reporting.outbox import ( +from adcp_reporting_storage import ( PgReportingOutbox, + PgReportingReconciliationStore, PgStatusNotificationStore, - ReportingNotificationError, ) -from adcp.reporting.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema -from adcp.reporting.outbox.status_schema import ( +from adcp_reporting_storage.outbox._schema import REQUIRED_OBJECTS, schema_objects, validate_schema +from adcp_reporting_storage.outbox.status_schema import ( REQUIRED_STATUS_OBJECTS, REQUIRED_STATUS_SELECTOR_OBJECTS, validate_status_schema, ) +from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller +from adcp.reporting.ledger.status_projection import lifecycle_intents, mismatch_key +from adcp.reporting.outbox import ReportingNotificationError + from . import test_reporting_notification_process_matrix as _process from ._generation_support import ( NOW, @@ -50,7 +50,11 @@ "a": "17ee407ae3978c8a2bb54437287afbf9dafb8130", "b": "0f34c666ac1961e9832fce43ef0ef6937b3c1dde", } -SQL = files("adcp.reporting.ledger").joinpath("reporting_status_notifications.sql").read_text() +SQL = ( + files("adcp_reporting_storage.ledger") + .joinpath("reporting_status_notifications.sql") + .read_text() +) C_QUEUES = ( "reporting_status_notification_events", "reporting_status_notification_expansions", @@ -209,7 +213,7 @@ async def test_populated_repeated_c_manifest_preserves_every_a_b_object_and_row( } assert ( json.dumps(REQUIRED_STATUS_OBJECTS, sort_keys=True, indent=2) + "\n" - == files("adcp.reporting.outbox") + == files("adcp_reporting_storage.outbox") .joinpath("required_status_schema.json") .read_text() ) @@ -277,12 +281,11 @@ async def migrate(selected): @pytest.mark.parametrize("release", ["a", "b"]) @case_deadline async def test_actual_unowned_artifact_requires_stopped_worker_migration(release, actual_sources): - from psycopg import sql - - from adcp.reporting.migration import ( + from adcp_reporting_storage.migration import ( ReportingOwnershipMigrationError, migrate_legacy_reporting, ) + from psycopg import sql async with isolated_reporting_pool(autocommit=True) as pool: async with service_process( diff --git a/tests/conformance/reporting/test_reporting_status_process_matrix.py b/tests/conformance/reporting/test_reporting_status_process_matrix.py index e3c8e2d4d..4adf13bc7 100644 --- a/tests/conformance/reporting/test_reporting_status_process_matrix.py +++ b/tests/conformance/reporting/test_reporting_status_process_matrix.py @@ -6,16 +6,13 @@ from datetime import timedelta import pytest +from adcp_reporting_storage import PgReportingReconciliationStore, PgStatusNotificationStore +from adcp_reporting_storage.ledger.status_snapshot import read_snapshot_on +from adcp_reporting_storage.outbox.status_pg import _replay_storage -from adcp.reporting.ledger import ( - PgReportingReconciliationStore, - ReportingDeliveryEscalation, - ReportingScheduleSpec, -) -from adcp.reporting.ledger.status_snapshot import read_snapshot_on -from adcp.reporting.outbox import PgStatusNotificationStore, ReportingStatusSweeper +from adcp.reporting.ledger import ReportingDeliveryEscalation, ReportingScheduleSpec +from adcp.reporting.outbox import ReportingStatusSweeper from adcp.reporting.outbox.status import escalation_identity -from adcp.reporting.outbox.status_pg import _replay_storage from ._generation_support import ( configuration, diff --git a/tests/conformance/reporting/test_reporting_status_projection_contract.py b/tests/conformance/reporting/test_reporting_status_projection_contract.py index 46c3f776e..a31314161 100644 --- a/tests/conformance/reporting/test_reporting_status_projection_contract.py +++ b/tests/conformance/reporting/test_reporting_status_projection_contract.py @@ -7,6 +7,7 @@ from datetime import timedelta import pytest +from adcp_reporting_storage import PgStatusNotificationStore from adcp.reporting.ledger import InMemoryReportingLedgerStore from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller @@ -14,7 +15,6 @@ from adcp.reporting.ledger.status_projection import StatusProjectionInput, project_status_scope from adcp.reporting.outbox import ( InMemoryStatusNotificationStore, - PgStatusNotificationStore, ReportingStatusScope, ReportingStatusSweeper, StatusChanged, diff --git a/tests/conformance/reporting/test_reporting_status_restatement_transactions.py b/tests/conformance/reporting/test_reporting_status_restatement_transactions.py index 2752781ca..da499092e 100644 --- a/tests/conformance/reporting/test_reporting_status_restatement_transactions.py +++ b/tests/conformance/reporting/test_reporting_status_restatement_transactions.py @@ -3,9 +3,9 @@ import asyncio import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgStatusNotificationStore -from adcp.reporting.ledger import PgReportingLedgerStore, RestatementCheckpoint -from adcp.reporting.outbox import PgStatusNotificationStore +from adcp.reporting.ledger import RestatementCheckpoint from . import test_reporting_status_projection_contract as _contract from ._generation_support import configuration, isolated_reporting_pool, obligation_for diff --git a/tests/conformance/reporting/test_reporting_status_selector_epoch.py b/tests/conformance/reporting/test_reporting_status_selector_epoch.py index 790ca17ce..96d1e3615 100644 --- a/tests/conformance/reporting/test_reporting_status_selector_epoch.py +++ b/tests/conformance/reporting/test_reporting_status_selector_epoch.py @@ -113,7 +113,9 @@ async def two_feeds(h, *, issue=True): async def test_directional_feed_public_private_configuration_obligation_scopes(status_harness): h = status_harness owners, issues = await two_feeds(h) - from adcp.reporting.ledger.status_snapshot import memory_snapshot, read_snapshot_on + from adcp_reporting_storage.ledger.status_snapshot import read_snapshot_on + + from adcp.reporting.ledger.status_snapshot import memory_snapshot if isinstance(h.ledger, InMemoryReportingLedgerStore): snapshot = memory_snapshot(h.ledger, "acct_a") @@ -275,7 +277,9 @@ async def test_checkpoint_event_failure_rolls_back_epoch_turn_and_restart_is_onc status_scope=ReportingStatusScope("acct_a"), ) before = await h.status.checkpoints(account_id="acct_a") - from adcp.reporting.outbox import status_memory, status_pg + from adcp_reporting_storage.outbox import status_pg + + from adcp.reporting.outbox import status_memory original = status_pg.advance_checkpoint called = 0 @@ -355,7 +359,9 @@ async def test_retained_scopes_outside_current_discovery_are_reprojected_without await c.execute( "UPDATE reporting_status_scope_checkpoints SET next_due_at=%s", (expired,) ) - from adcp.reporting.outbox import status_memory, status_pg + from adcp_reporting_storage.outbox import status_pg + + from adcp.reporting.outbox import status_memory with monkeypatch.context() as patch: patch.setattr(status_memory, "projection_scopes", lambda snapshot: ()) diff --git a/tests/conformance/reporting/test_reporting_status_selector_pg.py b/tests/conformance/reporting/test_reporting_status_selector_pg.py index b43d95b6f..b04938180 100644 --- a/tests/conformance/reporting/test_reporting_status_selector_pg.py +++ b/tests/conformance/reporting/test_reporting_status_selector_pg.py @@ -10,14 +10,14 @@ from pathlib import Path import pytest +from adcp_reporting_storage import PgReportingReconciliationStore +from adcp_reporting_storage.outbox.status_schema import validate_status_schema -from adcp.reporting.ledger import PgReportingReconciliationStore from adcp.reporting.outbox import ( ActivityRequest, ReportingEnvelopeCipher, ReportingNotificationWorker, ) -from adcp.reporting.outbox.status_schema import validate_status_schema from ._generation_support import ( NOW, @@ -156,10 +156,9 @@ async def populate_status_activity(status): async def test_actual_c_status_rows_are_archived_after_worker_stop(frozen_c): + from adcp_reporting_storage.migration import migrate_legacy_reporting from psycopg import sql - from adcp.reporting.migration import migrate_legacy_reporting - async with isolated_reporting_pool(autocommit=True) as pool: async with old_c(pool, frozen_c) as old: assert await old.call(action="schema") == {"result": True} diff --git a/tests/conformance/reporting/test_reporting_status_transactions.py b/tests/conformance/reporting/test_reporting_status_transactions.py index 830cbd76a..9ebd44242 100644 --- a/tests/conformance/reporting/test_reporting_status_transactions.py +++ b/tests/conformance/reporting/test_reporting_status_transactions.py @@ -5,8 +5,8 @@ from datetime import timedelta import pytest +from adcp_reporting_storage import PgReportingLedgerStore, PgStatusNotificationStore -from adcp.reporting.ledger import PgReportingLedgerStore from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller from adcp.reporting.ledger.status import ReportingStatusCaller, ReportingStatusHandler from adcp.reporting.ledger.status_projection import ( @@ -14,11 +14,7 @@ mismatch_key, project_status_scope, ) -from adcp.reporting.outbox import ( - PgStatusNotificationStore, - ReportingStatusScope, - ReportingStatusSweeper, -) +from adcp.reporting.outbox import ReportingStatusScope, ReportingStatusSweeper from . import test_reporting_status_projection_contract as _contract from ._generation_support import ( @@ -130,7 +126,9 @@ async def test_consumer_status_pre_lifecycle_crash_rolls_back_both_and_self_dirt await h.status.baseline(account_id="acct_a") outbox = NotificationHarness(h.reliable).outbox before = await outbox.read_status_dirty(account_id="acct_a") - import adcp.reporting.ledger.status_snapshot as snapshots + import adcp_reporting_storage.ledger.status_snapshot as snapshots + + import adcp.reporting.ledger.status_snapshot as memory_snapshots def crash_memory(*args, **kwargs): raise SimulatedCrash("consumer_status_pre_lifecycle") @@ -144,7 +142,7 @@ async def crash_pg(*args, **kwargs): reporting_revision_id=revision.reporting_revision_id, ) with monkeypatch.context() as patch: - patch.setattr(snapshots, "apply_memory_intents", crash_memory) + patch.setattr(memory_snapshots, "apply_memory_intents", crash_memory) patch.setattr(snapshots, "apply_intents_on", crash_pg) with pytest.raises(SimulatedCrash): await h.ledger.record_consumer_status_with_lifecycle(record) @@ -185,7 +183,9 @@ async def test_fanout_failure_cannot_advance_half_a_source_transaction(status_ha reporting_revision_id=revision.reporting_revision_id, readable=False, ) - from adcp.reporting.outbox import status_memory, status_pg + from adcp_reporting_storage.outbox import status_pg + + from adcp.reporting.outbox import status_memory called = 0 original = status_pg.advance_checkpoint @@ -224,7 +224,9 @@ async def test_expiry_during_checkpoint_event_work_rolls_back_the_entire_turn( h.clock.now = obligation.period.expected_at lease = await h.status.claim_due(account_id="acct_a", lease_seconds=30) assert lease is not None - from adcp.reporting.outbox import status_memory, status_pg + from adcp_reporting_storage.outbox import status_pg + + from adcp.reporting.outbox import status_memory original = status_pg.advance_checkpoint diff --git a/tests/conformance/reporting/test_reporting_status_waiver_rc6.py b/tests/conformance/reporting/test_reporting_status_waiver_rc6.py index 644356e5b..cc30d52fa 100644 --- a/tests/conformance/reporting/test_reporting_status_waiver_rc6.py +++ b/tests/conformance/reporting/test_reporting_status_waiver_rc6.py @@ -3,12 +3,13 @@ from dataclasses import replace import pytest +from adcp_reporting_storage import PgStatusNotificationStore +from adcp_reporting_storage.outbox.status_schema import validate_status_schema from adcp.reporting.ledger import ReportingStatusCaller as OwnershipCaller from adcp.reporting.ledger.status import ReportingStatusCaller, ReportingStatusHandler from adcp.reporting.ledger.status_projection import mismatch_key -from adcp.reporting.outbox import PgStatusNotificationStore, ReportingNotificationError -from adcp.reporting.outbox.status_schema import validate_status_schema +from adcp.reporting.outbox import ReportingNotificationError from . import test_reporting_status_projection_contract as _status_contract from ._generation_support import configuration, isolated_reporting_pool, revision_for @@ -178,7 +179,7 @@ async def test_successive_waivers_preserve_each_terminal_record_and_other_caller @pytest.mark.parametrize("column", ["reporting_status_id", "conflict_sha256"]) async def test_missing_waiver_binding_column_fails_readiness(column): - from adcp.reporting.ledger import PgReportingLedgerStore + from adcp_reporting_storage import PgReportingLedgerStore async with isolated_reporting_pool(autocommit=True) as pool: status = PgStatusNotificationStore(PgReportingLedgerStore(pool=pool, notifications=True)) diff --git a/tests/fixtures/public_api_snapshot.json b/tests/fixtures/public_api_snapshot.json index e5cbc964a..084450cae 100644 --- a/tests/fixtures/public_api_snapshot.json +++ b/tests/fixtures/public_api_snapshot.json @@ -826,18 +826,12 @@ "LedgerPage": "adcp.reporting.ledger.store", "LedgerSnapshot": "adcp.reporting.ledger.models", "ObligationProjection": "adcp.reporting.ledger.health", - "PgReportingLedgerStore": "adcp.reporting.ledger.pg", - "PgReportingReconciliationStore": "adcp.reporting.ledger.delivery_pg", "ProducerOfferings": "adcp.reporting.ledger.producer", "REPORTING_SELECTOR_VERSION": "value:2", "ReportingAdjustmentReceiptRecord": "adcp.reporting.ledger.delivery_models", "ReportingAdjustmentRecord": "adcp.reporting.ledger.models", "ReportingCaller": "adcp.reporting.ledger.models", "ReportingCanonicalDigest": "adcp.reporting.evidence", - "ReportingChangeCursorExpiredError": "adcp.reporting.ledger.change_feed", - "ReportingChangeKindV1": "union[value:'adjustment',value:'obligation',value:'retirement',value:'revision']", - "ReportingChangeRecordV1": "adcp.reporting.ledger.change_feed", - "ReportingChangesPageV1": "adcp.reporting.ledger.change_feed", "ReportingConfiguration": "adcp.reporting.ledger.models", "ReportingConfigurationGenerationKey": "adcp.reporting.ledger.models", "ReportingControlTotalRecord": "adcp.reporting.evidence", @@ -863,13 +857,10 @@ "ReportingMaterializationStore": "adcp.reporting.ledger.delivery", "ReportingMaterializationView": "adcp.reporting.ledger.delivery", "ReportingMismatchCode": "union[value:'coverage_short',value:'currency_mismatch',value:'metric_missing',value:'period_mismatch',value:'schema_nonconformant',value:'scope_media_buy_missing']", - "ReportingObjectRowBindingV1": "adcp.reporting.ledger.row_storage", "ReportingObligationDeliveryRecord": "adcp.reporting.ledger.delivery_models", "ReportingObligationRecord": "adcp.reporting.ledger.models", "ReportingPeriodBoundary": "adcp.reporting.ledger.models", - "ReportingPeriodRetirementV1": "adcp.reporting.ledger.period_retirement", "ReportingPhysicalChecksum": "adcp.reporting.ledger.delivery_models", - "ReportingPostgresRowBindingV1": "adcp.reporting.ledger.row_storage", "ReportingProducer": "adcp.reporting.ledger.producer", "ReportingProductionStatus": "union[value:'failed',value:'not_due',value:'pending',value:'published']", "ReportingReceiptKey": "adcp.reporting.ledger.delivery_models", @@ -893,14 +884,12 @@ "ReportingRevisionRecord": "adcp.reporting.ledger.models", "ReportingRevisionSelected": "adcp.reporting.revision_selection", "ReportingRevisionSelection": "union[adcp.reporting.revision_selection:ReportingRevisionCorrupt,adcp.reporting.revision_selection:ReportingRevisionNotReady,union[instance:typing:TypeVar]]", - "ReportingRowBindingSelectionInputV1": "adcp.reporting.ledger.row_storage", - "ReportingRowIntentSweepV1": "adcp.reporting.ledger.row_retention", "ReportingRowPage": "adcp.reporting.ledger.store", - "ReportingRowPruneV1": "adcp.reporting.ledger.row_retention", - "ReportingRowStorageOptionsV1": "adcp.reporting.ledger.row_storage", + "ReportingRowStagingStore": "adcp.reporting.ledger.store", "ReportingRowStoreError": "adcp.reporting.ledger.row_storage_errors", "ReportingRowStoreErrorCode": "union[value:'ABORTED',value:'CONTENT_CONFLICT',value:'DEADLINE_EXCEEDED',value:'INVALID_INPUT',value:'PROVIDER_UNAVAILABLE',value:'ROWS_EXPIRED',value:'ROWS_INTEGRITY_FAILED',value:'ROWS_UNAVAILABLE',value:'STATE_UNAVAILABLE',value:'UNSAFE_BINDING']", "ReportingScheduleSpec": "adcp.reporting.ledger.models", + "ReportingSourcePublicationStore": "adcp.reporting.ledger.store", "ReportingStatusCaller": "adcp.reporting.ledger.status", "ReportingStatusCallerResolver": "union[instance:builtins:list,union[adcp.reporting.ledger.status:ReportingStatusCaller]]", "ReportingStatusHandler": "adcp.reporting.ledger.status", @@ -909,6 +898,7 @@ "ReportingStatusSnapshot": "adcp.reporting.ledger.status_projection", "ReportingStatusView": "union[value:'periods',value:'revision',value:'summary']", "ReportingVerificationRecord": "adcp.reporting.ledger.delivery_models", + "ReportingWorkerRetryPolicy": "adcp.reporting.ledger.store", "RestatementCheckpoint": "adcp.reporting.ledger.store", "RestatementCheckpointStore": "adcp.reporting.ledger.store", "RetryScheduleEntry": "adcp.reporting.ledger.store", @@ -8176,5 +8166,67 @@ "Type": "trusted_match.context_match_request", "VerificationLevel": "trusted_match.identity_match_request" } + }, + "adcp_reporting_storage": { + "resolves": { + "PgReportingActivityUnionStore": "adcp_reporting_storage.outbox.status_activity_pg", + "PgReportingFeedStore": "adcp_reporting_storage.feed.pg", + "PgReportingLedgerStore": "adcp_reporting_storage.ledger.pg", + "PgReportingMaterializerStore": "adcp_reporting_storage.materializer.pg", + "PgReportingOutbox": "adcp_reporting_storage.outbox.pg", + "PgReportingProductionOutbox": "adcp_reporting_storage.production.pg", + "PgReportingProductionStore": "adcp_reporting_storage.production.pg", + "PgReportingProjectionOutbox": "adcp_reporting_storage.projection.notifications", + "PgReportingProjectionStore": "adcp_reporting_storage.projection.pg", + "PgReportingReceiptStore": "adcp_reporting_storage.receipts.pg", + "PgReportingReconciliationStore": "adcp_reporting_storage.ledger.delivery_pg", + "PgReportingSealStore": "adcp_reporting_storage.inline_storage", + "PgReportingStagingStore": "adcp_reporting_storage.inline_storage", + "PgReportingStatusOutbox": "adcp_reporting_storage.outbox.status_pg", + "PgReportingStatusProjection": "adcp_reporting_storage.projection.pg", + "PgReportingSubmissionIntentStore": "adcp_reporting_storage.submissions.pg", + "PgStatusNotificationStore": "adcp_reporting_storage.outbox.status_pg", + "ReportingChangeCursorExpiredError": "adcp_reporting_storage.ledger.change_feed", + "ReportingChangeKindV1": "union[value:'adjustment',value:'obligation',value:'retirement',value:'revision']", + "ReportingChangeRecordV1": "adcp_reporting_storage.ledger.change_feed", + "ReportingChangesPageV1": "adcp_reporting_storage.ledger.change_feed", + "ReportingObjectRowBindingV1": "adcp_reporting_storage.ledger.row_storage", + "ReportingOwnershipBackfill": "adcp_reporting_storage.migration", + "ReportingOwnershipMigrationError": "adcp_reporting_storage.migration", + "ReportingPeriodRetirementV1": "adcp_reporting_storage.ledger.period_retirement", + "ReportingPostgresRowBindingV1": "adcp_reporting_storage.ledger.row_storage", + "ReportingRowBindingSelectionInputV1": "adcp_reporting_storage.ledger.row_storage", + "ReportingRowIntentSweepV1": "adcp_reporting_storage.ledger.row_retention", + "ReportingRowMigrationBatchV1": "adcp_reporting_storage.ledger.row_migration", + "ReportingRowObjectContextV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectDeleteInputV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectGetInputV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectProviderV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectPutInputV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectPutResultV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectStatInputV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowObjectStatResultV1": "adcp_reporting_storage.ledger.row_storage_object", + "ReportingRowPruneV1": "adcp_reporting_storage.ledger.row_retention", + "ReportingRowQuarantineReason": "union[value:'BINDING_MISMATCH',value:'ROWS_NOT_PORTABLE',value:'ROW_SEQUENCE_INVALID']", + "ReportingRowStorageOptionsV1": "adcp_reporting_storage.ledger.row_storage", + "ReportingRowStorageV1": "adcp_reporting_storage.ledger.row_storage", + "assert_gcs_reporting_row_bucket_policy_v1": "adcp_reporting_storage.ledger.row_storage_gcs", + "backfill_legacy_reporting": "adcp_reporting_storage.migration", + "create_azure_blob_reporting_row_object_provider_v1": "adcp_reporting_storage.ledger.row_storage_azure", + "create_filesystem_reporting_row_object_provider_v1": "adcp_reporting_storage.ledger.row_storage_object", + "create_gcs_reporting_row_object_provider_v1": "adcp_reporting_storage.ledger.row_storage_gcs", + "create_s3_reporting_row_object_provider_v1": "adcp_reporting_storage.ledger.row_storage_s3", + "legacy_generation_digest": "adcp_reporting_storage.migration", + "migrate_legacy_reporting": "adcp_reporting_storage.migration", + "postgres_reliable_reporting_service": "adcp_reporting_storage.service", + "render_reporting_row_object_key_v1": "adcp_reporting_storage.ledger.row_storage_object", + "replace_empty_legacy_reporting": "adcp_reporting_storage.migration", + "replace_empty_legacy_reporting_sync": "adcp_reporting_storage.migration", + "replace_empty_owned_reporting": "adcp_reporting_storage.migration", + "replace_empty_owned_reporting_sync": "adcp_reporting_storage.migration", + "reporting_period_date_v1": "adcp_reporting_storage.ledger.row_storage_object", + "require_owned_schema_or_empty": "adcp_reporting_storage.migration", + "run_reporting_row_object_provider_conformance_v1": "adcp_reporting_storage.ledger.row_storage_object" + } } } diff --git a/tests/fixtures/reporting-row-storage-interop-v1/js-written.json b/tests/fixtures/reporting-row-storage-interop-v1/js-written.json index aea1e1918..3751f645c 100644 --- a/tests/fixtures/reporting-row-storage-interop-v1/js-written.json +++ b/tests/fixtures/reporting-row-storage-interop-v1/js-written.json @@ -1,12926 +1,59 @@ { "expected": [ - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "69403a2a705dcce0c564849c26c55a899f0c1158dd12a56ce370b004069fd5fa", - "content_size_bytes": 344, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_single-row_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8", - "content_size_bytes": 345, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_single-row_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_single-row_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_single-row_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "39d5233af55a871656ea98bc512f3c6bc3446ff30a97747f26cd117913af83af", - "content_size_bytes": 467, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_unicode-and-numbers_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd", - "content_size_bytes": 468, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_unicode-and-numbers_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "de89268832ff75b1fe6f77a61de3c174ed49289172bde20d537b10c4b067fbc0", - "content_size_bytes": 10152, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_three-segments_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f", - "content_size_bytes": 10153, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_three-segments_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_three-segments_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_js_three-segments_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - } + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"69403a2a705dcce0c564849c26c55a899f0c1158dd12a56ce370b004069fd5fa","content_size_bytes":344,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_single-row_revision_inline","row_set_kind":"revision","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8","content_size_bytes":345,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_single-row_revision_reports","row_set_kind":"revision","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_single-row_adjustment_inline","row_set_kind":"adjustment","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_single-row_adjustment_reports","row_set_kind":"adjustment","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"39d5233af55a871656ea98bc512f3c6bc3446ff30a97747f26cd117913af83af","content_size_bytes":467,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_unicode-and-numbers_revision_inline","row_set_kind":"revision","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd","content_size_bytes":468,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_unicode-and-numbers_revision_reports","row_set_kind":"revision","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_unicode-and-numbers_adjustment_inline","row_set_kind":"adjustment","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_unicode-and-numbers_adjustment_reports","row_set_kind":"adjustment","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"de89268832ff75b1fe6f77a61de3c174ed49289172bde20d537b10c4b067fbc0","content_size_bytes":10152,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_three-segments_revision_inline","row_set_kind":"revision","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f","content_size_bytes":10153,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_three-segments_revision_reports","row_set_kind":"revision","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_three-segments_adjustment_inline","row_set_kind":"adjustment","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_js_three-segments_adjustment_reports","row_set_kind":"adjustment","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]} ], "state": { "chunk_bodies": [ - { - "account_id": "acct_interop", - "body_hex": "7b226f223a307d0a7b226f223a317d0a7b226f223a327d0a7b226f223a337d0a7b226f223a347d0a7b226f223a357d0a7b226f223a367d0a7b226f223a377d0a7b226f223a387d0a7b226f223a397d0a7b226f223a31307d0a7b226f223a31317d0a7b226f223a31327d0a7b226f223a31337d0a7b226f223a31347d0a7b226f223a31357d0a7b226f223a31367d0a7b226f223a31377d0a7b226f223a31387d0a7b226f223a31397d0a7b226f223a32307d0a7b226f223a32317d0a7b226f223a32327d0a7b226f223a32337d0a7b226f223a32347d0a7b226f223a32357d0a7b226f223a32367d0a7b226f223a32377d0a7b226f223a32387d0a7b226f223a32397d0a7b226f223a33307d0a7b226f223a33317d0a7b226f223a33327d0a7b226f223a33337d0a7b226f223a33347d0a7b226f223a33357d0a7b226f223a33367d0a7b226f223a33377d0a7b226f223a33387d0a7b226f223a33397d0a7b226f223a34307d0a7b226f223a34317d0a7b226f223a34327d0a7b226f223a34337d0a7b226f223a34347d0a7b226f223a34357d0a7b226f223a34367d0a7b226f223a34377d0a7b226f223a34387d0a7b226f223a34397d0a7b226f223a35307d0a7b226f223a35317d0a7b226f223a35327d0a7b226f223a35337d0a7b226f223a35347d0a7b226f223a35357d0a7b226f223a35367d0a7b226f223a35377d0a7b226f223a35387d0a7b226f223a35397d0a7b226f223a36307d0a7b226f223a36317d0a7b226f223a36327d0a7b226f223a36337d0a7b226f223a36347d0a7b226f223a36357d0a7b226f223a36367d0a7b226f223a36377d0a7b226f223a36387d0a7b226f223a36397d0a7b226f223a37307d0a7b226f223a37317d0a7b226f223a37327d0a7b226f223a37337d0a7b226f223a37347d0a7b226f223a37357d0a7b226f223a37367d0a7b226f223a37377d0a7b226f223a37387d0a7b226f223a37397d0a7b226f223a38307d0a7b226f223a38317d0a7b226f223a38327d0a7b226f223a38337d0a7b226f223a38347d0a7b226f223a38357d0a7b226f223a38367d0a7b226f223a38377d0a7b226f223a38387d0a7b226f223a38397d0a7b226f223a39307d0a7b226f223a39317d0a7b226f223a39327d0a7b226f223a39337d0a7b226f223a39347d0a7b226f223a39357d0a7b226f223a39367d0a7b226f223a39377d0a7b226f223a39387d0a7b226f223a39397d0a7b226f223a3130307d0a7b226f223a3130317d0a7b226f223a3130327d0a7b226f223a3130337d0a7b226f223a3130347d0a7b226f223a3130357d0a7b226f223a3130367d0a7b226f223a3130377d0a7b226f223a3130387d0a7b226f223a3130397d0a7b226f223a3131307d0a7b226f223a3131317d0a7b226f223a3131327d0a7b226f223a3131337d0a7b226f223a3131347d0a7b226f223a3131357d0a7b226f223a3131367d0a7b226f223a3131377d0a7b226f223a3131387d0a7b226f223a3131397d0a7b226f223a3132307d0a7b226f223a3132317d0a7b226f223a3132327d0a7b226f223a3132337d0a7b226f223a3132347d0a7b226f223a3132357d0a7b226f223a3132367d0a7b226f223a3132377d0a7b226f223a3132387d0a7b226f223a3132397d0a7b226f223a3133307d0a7b226f223a3133317d0a7b226f223a3133327d0a7b226f223a3133337d0a7b226f223a3133347d0a7b226f223a3133357d0a7b226f223a3133367d0a7b226f223a3133377d0a7b226f223a3133387d0a7b226f223a3133397d0a7b226f223a3134307d0a7b226f223a3134317d0a7b226f223a3134327d0a7b226f223a3134337d0a7b226f223a3134347d0a7b226f223a3134357d0a7b226f223a3134367d0a7b226f223a3134377d0a7b226f223a3134387d0a7b226f223a3134397d0a7b226f223a3135307d0a7b226f223a3135317d0a7b226f223a3135327d0a7b226f223a3135337d0a7b226f223a3135347d0a7b226f223a3135357d0a7b226f223a3135367d0a7b226f223a3135377d0a7b226f223a3135387d0a7b226f223a3135397d0a7b226f223a3136307d0a7b226f223a3136317d0a7b226f223a3136327d0a7b226f223a3136337d0a7b226f223a3136347d0a7b226f223a3136357d0a7b226f223a3136367d0a7b226f223a3136377d0a7b226f223a3136387d0a7b226f223a3136397d0a7b226f223a3137307d0a7b226f223a3137317d0a7b226f223a3137327d0a7b226f223a3137337d0a7b226f223a3137347d0a7b226f223a3137357d0a7b226f223a3137367d0a7b226f223a3137377d0a7b226f223a3137387d0a7b226f223a3137397d0a7b226f223a3138307d0a7b226f223a3138317d0a7b226f223a3138327d0a7b226f223a3138337d0a7b226f223a3138347d0a7b226f223a3138357d0a7b226f223a3138367d0a7b226f223a3138377d0a7b226f223a3138387d0a7b226f223a3138397d0a7b226f223a3139307d0a7b226f223a3139317d0a7b226f223a3139327d0a7b226f223a3139337d0a7b226f223a3139347d0a7b226f223a3139357d0a7b226f223a3139367d0a7b226f223a3139377d0a7b226f223a3139387d0a7b226f223a3139397d0a7b226f223a3230307d0a7b226f223a3230317d0a7b226f223a3230327d0a7b226f223a3230337d0a7b226f223a3230347d0a7b226f223a3230357d0a7b226f223a3230367d0a7b226f223a3230377d0a7b226f223a3230387d0a7b226f223a3230397d0a7b226f223a3231307d0a7b226f223a3231317d0a7b226f223a3231327d0a7b226f223a3231337d0a7b226f223a3231347d0a7b226f223a3231357d0a7b226f223a3231367d0a7b226f223a3231377d0a7b226f223a3231387d0a7b226f223a3231397d0a7b226f223a3232307d0a7b226f223a3232317d0a7b226f223a3232327d0a7b226f223a3232337d0a7b226f223a3232347d0a7b226f223a3232357d0a7b226f223a3232367d0a7b226f223a3232377d0a7b226f223a3232387d0a7b226f223a3232397d0a7b226f223a3233307d0a7b226f223a3233317d0a7b226f223a3233327d0a7b226f223a3233337d0a7b226f223a3233347d0a7b226f223a3233357d0a7b226f223a3233367d0a7b226f223a3233377d0a7b226f223a3233387d0a7b226f223a3233397d0a7b226f223a3234307d0a7b226f223a3234317d0a7b226f223a3234327d0a7b226f223a3234337d0a7b226f223a3234347d0a7b226f223a3234357d0a7b226f223a3234367d0a7b226f223a3234377d0a7b226f223a3234387d0a7b226f223a3234397d0a7b226f223a3235307d0a7b226f223a3235317d0a7b226f223a3235327d0a7b226f223a3235337d0a7b226f223a3235347d0a7b226f223a3235357d0a7b226f223a3235367d0a7b226f223a3235377d0a7b226f223a3235387d0a7b226f223a3235397d0a7b226f223a3236307d0a7b226f223a3236317d0a7b226f223a3236327d0a7b226f223a3236337d0a7b226f223a3236347d0a7b226f223a3236357d0a7b226f223a3236367d0a7b226f223a3236377d0a7b226f223a3236387d0a7b226f223a3236397d0a7b226f223a3237307d0a7b226f223a3237317d0a7b226f223a3237327d0a7b226f223a3237337d0a7b226f223a3237347d0a7b226f223a3237357d0a7b226f223a3237367d0a7b226f223a3237377d0a7b226f223a3237387d0a7b226f223a3237397d0a7b226f223a3238307d0a7b226f223a3238317d0a7b226f223a3238327d0a7b226f223a3238337d0a7b226f223a3238347d0a7b226f223a3238357d0a7b226f223a3238367d0a7b226f223a3238377d0a7b226f223a3238387d0a7b226f223a3238397d0a7b226f223a3239307d0a7b226f223a3239317d0a7b226f223a3239327d0a7b226f223a3239337d0a7b226f223a3239347d0a7b226f223a3239357d0a7b226f223a3239367d0a7b226f223a3239377d0a7b226f223a3239387d0a7b226f223a3239397d0a7b226f223a3330307d0a7b226f223a3330317d0a7b226f223a3330327d0a7b226f223a3330337d0a7b226f223a3330347d0a7b226f223a3330357d0a7b226f223a3330367d0a7b226f223a3330377d0a7b226f223a3330387d0a7b226f223a3330397d0a7b226f223a3331307d0a7b226f223a3331317d0a7b226f223a3331327d0a7b226f223a3331337d0a7b226f223a3331347d0a7b226f223a3331357d0a7b226f223a3331367d0a7b226f223a3331377d0a7b226f223a3331387d0a7b226f223a3331397d0a7b226f223a3332307d0a7b226f223a3332317d0a7b226f223a3332327d0a7b226f223a3332337d0a7b226f223a3332347d0a7b226f223a3332357d0a7b226f223a3332367d0a7b226f223a3332377d0a7b226f223a3332387d0a7b226f223a3332397d0a7b226f223a3333307d0a7b226f223a3333317d0a7b226f223a3333327d0a7b226f223a3333337d0a7b226f223a3333347d0a7b226f223a3333357d0a7b226f223a3333367d0a7b226f223a3333377d0a7b226f223a3333387d0a7b226f223a3333397d0a7b226f223a3334307d0a7b226f223a3334317d0a7b226f223a3334327d0a7b226f223a3334337d0a7b226f223a3334347d0a7b226f223a3334357d0a7b226f223a3334367d0a7b226f223a3334377d0a7b226f223a3334387d0a7b226f223a3334397d0a7b226f223a3335307d0a7b226f223a3335317d0a7b226f223a3335327d0a7b226f223a3335337d0a7b226f223a3335347d0a7b226f223a3335357d0a7b226f223a3335367d0a7b226f223a3335377d0a7b226f223a3335387d0a7b226f223a3335397d0a7b226f223a3336307d0a7b226f223a3336317d0a7b226f223a3336327d0a7b226f223a3336337d0a7b226f223a3336347d0a7b226f223a3336357d0a7b226f223a3336367d0a7b226f223a3336377d0a7b226f223a3336387d0a7b226f223a3336397d0a7b226f223a3337307d0a7b226f223a3337317d0a7b226f223a3337327d0a7b226f223a3337337d0a7b226f223a3337347d0a7b226f223a3337357d0a7b226f223a3337367d0a7b226f223a3337377d0a7b226f223a3337387d0a7b226f223a3337397d0a7b226f223a3338307d0a7b226f223a3338317d0a7b226f223a3338327d0a7b226f223a3338337d0a7b226f223a3338347d0a7b226f223a3338357d0a7b226f223a3338367d0a7b226f223a3338377d0a7b226f223a3338387d0a7b226f223a3338397d0a7b226f223a3339307d0a7b226f223a3339317d0a7b226f223a3339327d0a7b226f223a3339337d0a7b226f223a3339347d0a7b226f223a3339357d0a7b226f223a3339367d0a7b226f223a3339377d0a7b226f223a3339387d0a7b226f223a3339397d0a7b226f223a3430307d0a7b226f223a3430317d0a7b226f223a3430327d0a7b226f223a3430337d0a7b226f223a3430347d0a7b226f223a3430357d0a7b226f223a3430367d0a7b226f223a3430377d0a7b226f223a3430387d0a7b226f223a3430397d0a7b226f223a3431307d0a7b226f223a3431317d0a7b226f223a3431327d0a7b226f223a3431337d0a7b226f223a3431347d0a7b226f223a3431357d0a7b226f223a3431367d0a7b226f223a3431377d0a7b226f223a3431387d0a7b226f223a3431397d0a7b226f223a3432307d0a7b226f223a3432317d0a7b226f223a3432327d0a7b226f223a3432337d0a7b226f223a3432347d0a7b226f223a3432357d0a7b226f223a3432367d0a7b226f223a3432377d0a7b226f223a3432387d0a7b226f223a3432397d0a7b226f223a3433307d0a7b226f223a3433317d0a7b226f223a3433327d0a7b226f223a3433337d0a7b226f223a3433347d0a7b226f223a3433357d0a7b226f223a3433367d0a7b226f223a3433377d0a7b226f223a3433387d0a7b226f223a3433397d0a7b226f223a3434307d0a7b226f223a3434317d0a7b226f223a3434327d0a7b226f223a3434337d0a7b226f223a3434347d0a7b226f223a3434357d0a7b226f223a3434367d0a7b226f223a3434377d0a7b226f223a3434387d0a7b226f223a3434397d0a7b226f223a3435307d0a7b226f223a3435317d0a7b226f223a3435327d0a7b226f223a3435337d0a7b226f223a3435347d0a7b226f223a3435357d0a7b226f223a3435367d0a7b226f223a3435377d0a7b226f223a3435387d0a7b226f223a3435397d0a7b226f223a3436307d0a7b226f223a3436317d0a7b226f223a3436327d0a7b226f223a3436337d0a7b226f223a3436347d0a7b226f223a3436357d0a7b226f223a3436367d0a7b226f223a3436377d0a7b226f223a3436387d0a7b226f223a3436397d0a7b226f223a3437307d0a7b226f223a3437317d0a7b226f223a3437327d0a7b226f223a3437337d0a7b226f223a3437347d0a7b226f223a3437357d0a7b226f223a3437367d0a7b226f223a3437377d0a7b226f223a3437387d0a7b226f223a3437397d0a7b226f223a3438307d0a7b226f223a3438317d0a7b226f223a3438327d0a7b226f223a3438337d0a7b226f223a3438347d0a7b226f223a3438357d0a7b226f223a3438367d0a7b226f223a3438377d0a7b226f223a3438387d0a7b226f223a3438397d0a7b226f223a3439307d0a7b226f223a3439317d0a7b226f223a3439327d0a7b226f223a3439337d0a7b226f223a3439347d0a7b226f223a3439357d0a7b226f223a3439367d0a7b226f223a3439377d0a7b226f223a3439387d0a7b226f223a3439397d0a7b226f223a3530307d0a7b226f223a3530317d0a7b226f223a3530327d0a7b226f223a3530337d0a7b226f223a3530347d0a7b226f223a3530357d0a7b226f223a3530367d0a7b226f223a3530377d0a7b226f223a3530387d0a7b226f223a3530397d0a7b226f223a3531307d0a7b226f223a3531317d0a7b226f223a3531327d0a7b226f223a3531337d0a7b226f223a3531347d0a7b226f223a3531357d0a7b226f223a3531367d0a7b226f223a3531377d0a7b226f223a3531387d0a7b226f223a3531397d0a7b226f223a3532307d0a7b226f223a3532317d0a7b226f223a3532327d0a7b226f223a3532337d0a7b226f223a3532347d0a7b226f223a3532357d0a7b226f223a3532367d0a7b226f223a3532377d0a7b226f223a3532387d0a7b226f223a3532397d0a7b226f223a3533307d0a7b226f223a3533317d0a7b226f223a3533327d0a7b226f223a3533337d0a7b226f223a3533347d0a7b226f223a3533357d0a7b226f223a3533367d0a7b226f223a3533377d0a7b226f223a3533387d0a7b226f223a3533397d0a7b226f223a3534307d0a7b226f223a3534317d0a7b226f223a3534327d0a7b226f223a3534337d0a7b226f223a3534347d0a7b226f223a3534357d0a7b226f223a3534367d0a7b226f223a3534377d0a7b226f223a3534387d0a7b226f223a3534397d0a7b226f223a3535307d0a7b226f223a3535317d0a7b226f223a3535327d0a7b226f223a3535337d0a7b226f223a3535347d0a7b226f223a3535357d0a7b226f223a3535367d0a7b226f223a3535377d0a7b226f223a3535387d0a7b226f223a3535397d0a7b226f223a3536307d0a7b226f223a3536317d0a7b226f223a3536327d0a7b226f223a3536337d0a7b226f223a3536347d0a7b226f223a3536357d0a7b226f223a3536367d0a7b226f223a3536377d0a7b226f223a3536387d0a7b226f223a3536397d0a7b226f223a3537307d0a7b226f223a3537317d0a7b226f223a3537327d0a7b226f223a3537337d0a7b226f223a3537347d0a7b226f223a3537357d0a7b226f223a3537367d0a7b226f223a3537377d0a7b226f223a3537387d0a7b226f223a3537397d0a7b226f223a3538307d0a7b226f223a3538317d0a7b226f223a3538327d0a7b226f223a3538337d0a7b226f223a3538347d0a7b226f223a3538357d0a7b226f223a3538367d0a7b226f223a3538377d0a7b226f223a3538387d0a7b226f223a3538397d0a7b226f223a3539307d0a7b226f223a3539317d0a7b226f223a3539327d0a7b226f223a3539337d0a7b226f223a3539347d0a7b226f223a3539357d0a7b226f223a3539367d0a7b226f223a3539377d0a7b226f223a3539387d0a7b226f223a3539397d0a7b226f223a3630307d0a7b226f223a3630317d0a7b226f223a3630327d0a7b226f223a3630337d0a7b226f223a3630347d0a7b226f223a3630357d0a7b226f223a3630367d0a7b226f223a3630377d0a7b226f223a3630387d0a7b226f223a3630397d0a7b226f223a3631307d0a7b226f223a3631317d0a7b226f223a3631327d0a7b226f223a3631337d0a7b226f223a3631347d0a7b226f223a3631357d0a7b226f223a3631367d0a7b226f223a3631377d0a7b226f223a3631387d0a7b226f223a3631397d0a7b226f223a3632307d0a7b226f223a3632317d0a7b226f223a3632327d0a7b226f223a3632337d0a7b226f223a3632347d0a7b226f223a3632357d0a7b226f223a3632367d0a7b226f223a3632377d0a7b226f223a3632387d0a7b226f223a3632397d0a7b226f223a3633307d0a7b226f223a3633317d0a7b226f223a3633327d0a7b226f223a3633337d0a7b226f223a3633347d0a7b226f223a3633357d0a7b226f223a3633367d0a7b226f223a3633377d0a7b226f223a3633387d0a7b226f223a3633397d0a7b226f223a3634307d0a7b226f223a3634317d0a7b226f223a3634327d0a7b226f223a3634337d0a7b226f223a3634347d0a7b226f223a3634357d0a7b226f223a3634367d0a7b226f223a3634377d0a7b226f223a3634387d0a7b226f223a3634397d0a7b226f223a3635307d0a7b226f223a3635317d0a7b226f223a3635327d0a7b226f223a3635337d0a7b226f223a3635347d0a7b226f223a3635357d0a7b226f223a3635367d0a7b226f223a3635377d0a7b226f223a3635387d0a7b226f223a3635397d0a7b226f223a3636307d0a7b226f223a3636317d0a7b226f223a3636327d0a7b226f223a3636337d0a7b226f223a3636347d0a7b226f223a3636357d0a7b226f223a3636367d0a7b226f223a3636377d0a7b226f223a3636387d0a7b226f223a3636397d0a7b226f223a3637307d0a7b226f223a3637317d0a7b226f223a3637327d0a7b226f223a3637337d0a7b226f223a3637347d0a7b226f223a3637357d0a7b226f223a3637367d0a7b226f223a3637377d0a7b226f223a3637387d0a7b226f223a3637397d0a7b226f223a3638307d0a7b226f223a3638317d0a7b226f223a3638327d0a7b226f223a3638337d0a7b226f223a3638347d0a7b226f223a3638357d0a7b226f223a3638367d0a7b226f223a3638377d0a7b226f223a3638387d0a7b226f223a3638397d0a7b226f223a3639307d0a7b226f223a3639317d0a7b226f223a3639327d0a7b226f223a3639337d0a7b226f223a3639347d0a7b226f223a3639357d0a7b226f223a3639367d0a7b226f223a3639377d0a7b226f223a3639387d0a7b226f223a3639397d0a7b226f223a3730307d0a7b226f223a3730317d0a7b226f223a3730327d0a7b226f223a3730337d0a7b226f223a3730347d0a7b226f223a3730357d0a7b226f223a3730367d0a7b226f223a3730377d0a7b226f223a3730387d0a7b226f223a3730397d0a7b226f223a3731307d0a7b226f223a3731317d0a7b226f223a3731327d0a7b226f223a3731337d0a7b226f223a3731347d0a7b226f223a3731357d0a7b226f223a3731367d0a7b226f223a3731377d0a7b226f223a3731387d0a7b226f223a3731397d0a7b226f223a3732307d0a7b226f223a3732317d0a7b226f223a3732327d0a7b226f223a3732337d0a7b226f223a3732347d0a7b226f223a3732357d0a7b226f223a3732367d0a7b226f223a3732377d0a7b226f223a3732387d0a7b226f223a3732397d0a7b226f223a3733307d0a7b226f223a3733317d0a7b226f223a3733327d0a7b226f223a3733337d0a7b226f223a3733347d0a7b226f223a3733357d0a7b226f223a3733367d0a7b226f223a3733377d0a7b226f223a3733387d0a7b226f223a3733397d0a7b226f223a3734307d0a7b226f223a3734317d0a7b226f223a3734327d0a7b226f223a3734337d0a7b226f223a3734347d0a7b226f223a3734357d0a7b226f223a3734367d0a7b226f223a3734377d0a7b226f223a3734387d0a7b226f223a3734397d0a7b226f223a3735307d0a7b226f223a3735317d0a7b226f223a3735327d0a7b226f223a3735337d0a7b226f223a3735347d0a7b226f223a3735357d0a7b226f223a3735367d0a7b226f223a3735377d0a7b226f223a3735387d0a7b226f223a3735397d0a7b226f223a3736307d0a7b226f223a3736317d0a7b226f223a3736327d0a7b226f223a3736337d0a7b226f223a3736347d0a7b226f223a3736357d0a7b226f223a3736367d0a7b226f223a3736377d0a7b226f223a3736387d0a7b226f223a3736397d0a7b226f223a3737307d0a7b226f223a3737317d0a7b226f223a3737327d0a7b226f223a3737337d0a7b226f223a3737347d0a7b226f223a3737357d0a7b226f223a3737367d0a7b226f223a3737377d0a7b226f223a3737387d0a7b226f223a3737397d0a7b226f223a3738307d0a7b226f223a3738317d0a7b226f223a3738327d0a7b226f223a3738337d0a7b226f223a3738347d0a7b226f223a3738357d0a7b226f223a3738367d0a7b226f223a3738377d0a7b226f223a3738387d0a7b226f223a3738397d0a7b226f223a3739307d0a7b226f223a3739317d0a7b226f223a3739327d0a7b226f223a3739337d0a7b226f223a3739347d0a7b226f223a3739357d0a7b226f223a3739367d0a7b226f223a3739377d0a7b226f223a3739387d0a7b226f223a3739397d0a7b226f223a3830307d0a7b226f223a3830317d0a7b226f223a3830327d0a7b226f223a3830337d0a7b226f223a3830347d0a7b226f223a3830357d0a7b226f223a3830367d0a7b226f223a3830377d0a7b226f223a3830387d0a7b226f223a3830397d0a7b226f223a3831307d0a7b226f223a3831317d0a7b226f223a3831327d0a7b226f223a3831337d0a7b226f223a3831347d0a7b226f223a3831357d0a7b226f223a3831367d0a7b226f223a3831377d0a7b226f223a3831387d0a7b226f223a3831397d0a7b226f223a3832307d0a7b226f223a3832317d0a7b226f223a3832327d0a7b226f223a3832337d0a7b226f223a3832347d0a7b226f223a3832357d0a7b226f223a3832367d0a7b226f223a3832377d0a7b226f223a3832387d0a7b226f223a3832397d0a7b226f223a3833307d0a7b226f223a3833317d0a7b226f223a3833327d0a7b226f223a3833337d0a7b226f223a3833347d0a7b226f223a3833357d0a7b226f223a3833367d0a7b226f223a3833377d0a7b226f223a3833387d0a7b226f223a3833397d0a7b226f223a3834307d0a7b226f223a3834317d0a7b226f223a3834327d0a7b226f223a3834337d0a7b226f223a3834347d0a7b226f223a3834357d0a7b226f223a3834367d0a7b226f223a3834377d0a7b226f223a3834387d0a7b226f223a3834397d0a7b226f223a3835307d0a7b226f223a3835317d0a7b226f223a3835327d0a7b226f223a3835337d0a7b226f223a3835347d0a7b226f223a3835357d0a7b226f223a3835367d0a7b226f223a3835377d0a7b226f223a3835387d0a7b226f223a3835397d0a7b226f223a3836307d0a7b226f223a3836317d0a7b226f223a3836327d0a7b226f223a3836337d0a7b226f223a3836347d0a7b226f223a3836357d0a7b226f223a3836367d0a7b226f223a3836377d0a7b226f223a3836387d0a7b226f223a3836397d0a7b226f223a3837307d0a7b226f223a3837317d0a7b226f223a3837327d0a7b226f223a3837337d0a7b226f223a3837347d0a7b226f223a3837357d0a7b226f223a3837367d0a7b226f223a3837377d0a7b226f223a3837387d0a7b226f223a3837397d0a7b226f223a3838307d0a7b226f223a3838317d0a7b226f223a3838327d0a7b226f223a3838337d0a7b226f223a3838347d0a7b226f223a3838357d0a7b226f223a3838367d0a7b226f223a3838377d0a7b226f223a3838387d0a7b226f223a3838397d0a7b226f223a3839307d0a7b226f223a3839317d0a7b226f223a3839327d0a7b226f223a3839337d0a7b226f223a3839347d0a7b226f223a3839357d0a7b226f223a3839367d0a7b226f223a3839377d0a7b226f223a3839387d0a7b226f223a3839397d0a7b226f223a3930307d0a7b226f223a3930317d0a7b226f223a3930327d0a7b226f223a3930337d0a7b226f223a3930347d0a7b226f223a3930357d0a7b226f223a3930367d0a7b226f223a3930377d0a7b226f223a3930387d0a7b226f223a3930397d0a7b226f223a3931307d0a7b226f223a3931317d0a7b226f223a3931327d0a7b226f223a3931337d0a7b226f223a3931347d0a7b226f223a3931357d0a7b226f223a3931367d0a7b226f223a3931377d0a7b226f223a3931387d0a7b226f223a3931397d0a7b226f223a3932307d0a7b226f223a3932317d0a7b226f223a3932327d0a7b226f223a3932337d0a7b226f223a3932347d0a7b226f223a3932357d0a7b226f223a3932367d0a7b226f223a3932377d0a7b226f223a3932387d0a7b226f223a3932397d0a7b226f223a3933307d0a7b226f223a3933317d0a7b226f223a3933327d0a7b226f223a3933337d0a7b226f223a3933347d0a7b226f223a3933357d0a7b226f223a3933367d0a7b226f223a3933377d0a7b226f223a3933387d0a7b226f223a3933397d0a7b226f223a3934307d0a7b226f223a3934317d0a7b226f223a3934327d0a7b226f223a3934337d0a7b226f223a3934347d0a7b226f223a3934357d0a7b226f223a3934367d0a7b226f223a3934377d0a7b226f223a3934387d0a7b226f223a3934397d0a7b226f223a3935307d0a7b226f223a3935317d0a7b226f223a3935327d0a7b226f223a3935337d0a7b226f223a3935347d0a7b226f223a3935357d0a7b226f223a3935367d0a7b226f223a3935377d0a7b226f223a3935387d0a7b226f223a3935397d0a7b226f223a3936307d0a7b226f223a3936317d0a7b226f223a3936327d0a7b226f223a3936337d0a7b226f223a3936347d0a7b226f223a3936357d0a7b226f223a3936367d0a7b226f223a3936377d0a7b226f223a3936387d0a7b226f223a3936397d0a7b226f223a3937307d0a7b226f223a3937317d0a7b226f223a3937327d0a7b226f223a3937337d0a7b226f223a3937347d0a7b226f223a3937357d0a7b226f223a3937367d0a7b226f223a3937377d0a7b226f223a3937387d0a7b226f223a3937397d0a7b226f223a3938307d0a7b226f223a3938317d0a7b226f223a3938327d0a7b226f223a3938337d0a7b226f223a3938347d0a7b226f223a3938357d0a7b226f223a3938367d0a7b226f223a3938377d0a7b226f223a3938387d0a7b226f223a3938397d0a7b226f223a3939307d0a7b226f223a3939317d0a7b226f223a3939327d0a7b226f223a3939337d0a7b226f223a3939347d0a7b226f223a3939357d0a7b226f223a3939367d0a7b226f223a3939377d0a7b226f223a3939387d0a7b226f223a3939397d0a7b226f223a313030307d0a", - "obligation_id": "robl_interop", - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277" - }, - { - "account_id": "acct_interop", - "body_hex": "7b2261223a322c22636f6e74726f6c223a225c75303030375c6e5c225c5c222c226e616d65223a22636166c3a920f09f988020e697a5e69cace8aa9e222c22736570223a22e280a8222c227a223a317d0a7b22666c6f6174223a302e33303030303030303030303030303030342c226d61785f696e74223a393030373139393235343734303939312c226e223a302c226e6567223a2d31322e352c226e6573746564223a7b2261223a5b5d2c227a223a5b312c7b2261223a747275652c2262223a6e756c6c7d5d7d2c2274696e79223a31652d377d0a", - "obligation_id": "robl_interop", - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - }, - { - "account_id": "acct_interop", - "body_hex": "7b2264617465223a22323032362d31302d3031222c22696d7072657373696f6e73223a313230302c226d656469615f6275795f6964223a226d625f31222c227061636b6167655f6964223a22706b675f31222c227370656e64223a2234322e3137227d0a", - "obligation_id": "robl_interop", - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } + {"account_id":"acct_interop","body_hex":"7b226f223a307d0a7b226f223a317d0a7b226f223a327d0a7b226f223a337d0a7b226f223a347d0a7b226f223a357d0a7b226f223a367d0a7b226f223a377d0a7b226f223a387d0a7b226f223a397d0a7b226f223a31307d0a7b226f223a31317d0a7b226f223a31327d0a7b226f223a31337d0a7b226f223a31347d0a7b226f223a31357d0a7b226f223a31367d0a7b226f223a31377d0a7b226f223a31387d0a7b226f223a31397d0a7b226f223a32307d0a7b226f223a32317d0a7b226f223a32327d0a7b226f223a32337d0a7b226f223a32347d0a7b226f223a32357d0a7b226f223a32367d0a7b226f223a32377d0a7b226f223a32387d0a7b226f223a32397d0a7b226f223a33307d0a7b226f223a33317d0a7b226f223a33327d0a7b226f223a33337d0a7b226f223a33347d0a7b226f223a33357d0a7b226f223a33367d0a7b226f223a33377d0a7b226f223a33387d0a7b226f223a33397d0a7b226f223a34307d0a7b226f223a34317d0a7b226f223a34327d0a7b226f223a34337d0a7b226f223a34347d0a7b226f223a34357d0a7b226f223a34367d0a7b226f223a34377d0a7b226f223a34387d0a7b226f223a34397d0a7b226f223a35307d0a7b226f223a35317d0a7b226f223a35327d0a7b226f223a35337d0a7b226f223a35347d0a7b226f223a35357d0a7b226f223a35367d0a7b226f223a35377d0a7b226f223a35387d0a7b226f223a35397d0a7b226f223a36307d0a7b226f223a36317d0a7b226f223a36327d0a7b226f223a36337d0a7b226f223a36347d0a7b226f223a36357d0a7b226f223a36367d0a7b226f223a36377d0a7b226f223a36387d0a7b226f223a36397d0a7b226f223a37307d0a7b226f223a37317d0a7b226f223a37327d0a7b226f223a37337d0a7b226f223a37347d0a7b226f223a37357d0a7b226f223a37367d0a7b226f223a37377d0a7b226f223a37387d0a7b226f223a37397d0a7b226f223a38307d0a7b226f223a38317d0a7b226f223a38327d0a7b226f223a38337d0a7b226f223a38347d0a7b226f223a38357d0a7b226f223a38367d0a7b226f223a38377d0a7b226f223a38387d0a7b226f223a38397d0a7b226f223a39307d0a7b226f223a39317d0a7b226f223a39327d0a7b226f223a39337d0a7b226f223a39347d0a7b226f223a39357d0a7b226f223a39367d0a7b226f223a39377d0a7b226f223a39387d0a7b226f223a39397d0a7b226f223a3130307d0a7b226f223a3130317d0a7b226f223a3130327d0a7b226f223a3130337d0a7b226f223a3130347d0a7b226f223a3130357d0a7b226f223a3130367d0a7b226f223a3130377d0a7b226f223a3130387d0a7b226f223a3130397d0a7b226f223a3131307d0a7b226f223a3131317d0a7b226f223a3131327d0a7b226f223a3131337d0a7b226f223a3131347d0a7b226f223a3131357d0a7b226f223a3131367d0a7b226f223a3131377d0a7b226f223a3131387d0a7b226f223a3131397d0a7b226f223a3132307d0a7b226f223a3132317d0a7b226f223a3132327d0a7b226f223a3132337d0a7b226f223a3132347d0a7b226f223a3132357d0a7b226f223a3132367d0a7b226f223a3132377d0a7b226f223a3132387d0a7b226f223a3132397d0a7b226f223a3133307d0a7b226f223a3133317d0a7b226f223a3133327d0a7b226f223a3133337d0a7b226f223a3133347d0a7b226f223a3133357d0a7b226f223a3133367d0a7b226f223a3133377d0a7b226f223a3133387d0a7b226f223a3133397d0a7b226f223a3134307d0a7b226f223a3134317d0a7b226f223a3134327d0a7b226f223a3134337d0a7b226f223a3134347d0a7b226f223a3134357d0a7b226f223a3134367d0a7b226f223a3134377d0a7b226f223a3134387d0a7b226f223a3134397d0a7b226f223a3135307d0a7b226f223a3135317d0a7b226f223a3135327d0a7b226f223a3135337d0a7b226f223a3135347d0a7b226f223a3135357d0a7b226f223a3135367d0a7b226f223a3135377d0a7b226f223a3135387d0a7b226f223a3135397d0a7b226f223a3136307d0a7b226f223a3136317d0a7b226f223a3136327d0a7b226f223a3136337d0a7b226f223a3136347d0a7b226f223a3136357d0a7b226f223a3136367d0a7b226f223a3136377d0a7b226f223a3136387d0a7b226f223a3136397d0a7b226f223a3137307d0a7b226f223a3137317d0a7b226f223a3137327d0a7b226f223a3137337d0a7b226f223a3137347d0a7b226f223a3137357d0a7b226f223a3137367d0a7b226f223a3137377d0a7b226f223a3137387d0a7b226f223a3137397d0a7b226f223a3138307d0a7b226f223a3138317d0a7b226f223a3138327d0a7b226f223a3138337d0a7b226f223a3138347d0a7b226f223a3138357d0a7b226f223a3138367d0a7b226f223a3138377d0a7b226f223a3138387d0a7b226f223a3138397d0a7b226f223a3139307d0a7b226f223a3139317d0a7b226f223a3139327d0a7b226f223a3139337d0a7b226f223a3139347d0a7b226f223a3139357d0a7b226f223a3139367d0a7b226f223a3139377d0a7b226f223a3139387d0a7b226f223a3139397d0a7b226f223a3230307d0a7b226f223a3230317d0a7b226f223a3230327d0a7b226f223a3230337d0a7b226f223a3230347d0a7b226f223a3230357d0a7b226f223a3230367d0a7b226f223a3230377d0a7b226f223a3230387d0a7b226f223a3230397d0a7b226f223a3231307d0a7b226f223a3231317d0a7b226f223a3231327d0a7b226f223a3231337d0a7b226f223a3231347d0a7b226f223a3231357d0a7b226f223a3231367d0a7b226f223a3231377d0a7b226f223a3231387d0a7b226f223a3231397d0a7b226f223a3232307d0a7b226f223a3232317d0a7b226f223a3232327d0a7b226f223a3232337d0a7b226f223a3232347d0a7b226f223a3232357d0a7b226f223a3232367d0a7b226f223a3232377d0a7b226f223a3232387d0a7b226f223a3232397d0a7b226f223a3233307d0a7b226f223a3233317d0a7b226f223a3233327d0a7b226f223a3233337d0a7b226f223a3233347d0a7b226f223a3233357d0a7b226f223a3233367d0a7b226f223a3233377d0a7b226f223a3233387d0a7b226f223a3233397d0a7b226f223a3234307d0a7b226f223a3234317d0a7b226f223a3234327d0a7b226f223a3234337d0a7b226f223a3234347d0a7b226f223a3234357d0a7b226f223a3234367d0a7b226f223a3234377d0a7b226f223a3234387d0a7b226f223a3234397d0a7b226f223a3235307d0a7b226f223a3235317d0a7b226f223a3235327d0a7b226f223a3235337d0a7b226f223a3235347d0a7b226f223a3235357d0a7b226f223a3235367d0a7b226f223a3235377d0a7b226f223a3235387d0a7b226f223a3235397d0a7b226f223a3236307d0a7b226f223a3236317d0a7b226f223a3236327d0a7b226f223a3236337d0a7b226f223a3236347d0a7b226f223a3236357d0a7b226f223a3236367d0a7b226f223a3236377d0a7b226f223a3236387d0a7b226f223a3236397d0a7b226f223a3237307d0a7b226f223a3237317d0a7b226f223a3237327d0a7b226f223a3237337d0a7b226f223a3237347d0a7b226f223a3237357d0a7b226f223a3237367d0a7b226f223a3237377d0a7b226f223a3237387d0a7b226f223a3237397d0a7b226f223a3238307d0a7b226f223a3238317d0a7b226f223a3238327d0a7b226f223a3238337d0a7b226f223a3238347d0a7b226f223a3238357d0a7b226f223a3238367d0a7b226f223a3238377d0a7b226f223a3238387d0a7b226f223a3238397d0a7b226f223a3239307d0a7b226f223a3239317d0a7b226f223a3239327d0a7b226f223a3239337d0a7b226f223a3239347d0a7b226f223a3239357d0a7b226f223a3239367d0a7b226f223a3239377d0a7b226f223a3239387d0a7b226f223a3239397d0a7b226f223a3330307d0a7b226f223a3330317d0a7b226f223a3330327d0a7b226f223a3330337d0a7b226f223a3330347d0a7b226f223a3330357d0a7b226f223a3330367d0a7b226f223a3330377d0a7b226f223a3330387d0a7b226f223a3330397d0a7b226f223a3331307d0a7b226f223a3331317d0a7b226f223a3331327d0a7b226f223a3331337d0a7b226f223a3331347d0a7b226f223a3331357d0a7b226f223a3331367d0a7b226f223a3331377d0a7b226f223a3331387d0a7b226f223a3331397d0a7b226f223a3332307d0a7b226f223a3332317d0a7b226f223a3332327d0a7b226f223a3332337d0a7b226f223a3332347d0a7b226f223a3332357d0a7b226f223a3332367d0a7b226f223a3332377d0a7b226f223a3332387d0a7b226f223a3332397d0a7b226f223a3333307d0a7b226f223a3333317d0a7b226f223a3333327d0a7b226f223a3333337d0a7b226f223a3333347d0a7b226f223a3333357d0a7b226f223a3333367d0a7b226f223a3333377d0a7b226f223a3333387d0a7b226f223a3333397d0a7b226f223a3334307d0a7b226f223a3334317d0a7b226f223a3334327d0a7b226f223a3334337d0a7b226f223a3334347d0a7b226f223a3334357d0a7b226f223a3334367d0a7b226f223a3334377d0a7b226f223a3334387d0a7b226f223a3334397d0a7b226f223a3335307d0a7b226f223a3335317d0a7b226f223a3335327d0a7b226f223a3335337d0a7b226f223a3335347d0a7b226f223a3335357d0a7b226f223a3335367d0a7b226f223a3335377d0a7b226f223a3335387d0a7b226f223a3335397d0a7b226f223a3336307d0a7b226f223a3336317d0a7b226f223a3336327d0a7b226f223a3336337d0a7b226f223a3336347d0a7b226f223a3336357d0a7b226f223a3336367d0a7b226f223a3336377d0a7b226f223a3336387d0a7b226f223a3336397d0a7b226f223a3337307d0a7b226f223a3337317d0a7b226f223a3337327d0a7b226f223a3337337d0a7b226f223a3337347d0a7b226f223a3337357d0a7b226f223a3337367d0a7b226f223a3337377d0a7b226f223a3337387d0a7b226f223a3337397d0a7b226f223a3338307d0a7b226f223a3338317d0a7b226f223a3338327d0a7b226f223a3338337d0a7b226f223a3338347d0a7b226f223a3338357d0a7b226f223a3338367d0a7b226f223a3338377d0a7b226f223a3338387d0a7b226f223a3338397d0a7b226f223a3339307d0a7b226f223a3339317d0a7b226f223a3339327d0a7b226f223a3339337d0a7b226f223a3339347d0a7b226f223a3339357d0a7b226f223a3339367d0a7b226f223a3339377d0a7b226f223a3339387d0a7b226f223a3339397d0a7b226f223a3430307d0a7b226f223a3430317d0a7b226f223a3430327d0a7b226f223a3430337d0a7b226f223a3430347d0a7b226f223a3430357d0a7b226f223a3430367d0a7b226f223a3430377d0a7b226f223a3430387d0a7b226f223a3430397d0a7b226f223a3431307d0a7b226f223a3431317d0a7b226f223a3431327d0a7b226f223a3431337d0a7b226f223a3431347d0a7b226f223a3431357d0a7b226f223a3431367d0a7b226f223a3431377d0a7b226f223a3431387d0a7b226f223a3431397d0a7b226f223a3432307d0a7b226f223a3432317d0a7b226f223a3432327d0a7b226f223a3432337d0a7b226f223a3432347d0a7b226f223a3432357d0a7b226f223a3432367d0a7b226f223a3432377d0a7b226f223a3432387d0a7b226f223a3432397d0a7b226f223a3433307d0a7b226f223a3433317d0a7b226f223a3433327d0a7b226f223a3433337d0a7b226f223a3433347d0a7b226f223a3433357d0a7b226f223a3433367d0a7b226f223a3433377d0a7b226f223a3433387d0a7b226f223a3433397d0a7b226f223a3434307d0a7b226f223a3434317d0a7b226f223a3434327d0a7b226f223a3434337d0a7b226f223a3434347d0a7b226f223a3434357d0a7b226f223a3434367d0a7b226f223a3434377d0a7b226f223a3434387d0a7b226f223a3434397d0a7b226f223a3435307d0a7b226f223a3435317d0a7b226f223a3435327d0a7b226f223a3435337d0a7b226f223a3435347d0a7b226f223a3435357d0a7b226f223a3435367d0a7b226f223a3435377d0a7b226f223a3435387d0a7b226f223a3435397d0a7b226f223a3436307d0a7b226f223a3436317d0a7b226f223a3436327d0a7b226f223a3436337d0a7b226f223a3436347d0a7b226f223a3436357d0a7b226f223a3436367d0a7b226f223a3436377d0a7b226f223a3436387d0a7b226f223a3436397d0a7b226f223a3437307d0a7b226f223a3437317d0a7b226f223a3437327d0a7b226f223a3437337d0a7b226f223a3437347d0a7b226f223a3437357d0a7b226f223a3437367d0a7b226f223a3437377d0a7b226f223a3437387d0a7b226f223a3437397d0a7b226f223a3438307d0a7b226f223a3438317d0a7b226f223a3438327d0a7b226f223a3438337d0a7b226f223a3438347d0a7b226f223a3438357d0a7b226f223a3438367d0a7b226f223a3438377d0a7b226f223a3438387d0a7b226f223a3438397d0a7b226f223a3439307d0a7b226f223a3439317d0a7b226f223a3439327d0a7b226f223a3439337d0a7b226f223a3439347d0a7b226f223a3439357d0a7b226f223a3439367d0a7b226f223a3439377d0a7b226f223a3439387d0a7b226f223a3439397d0a7b226f223a3530307d0a7b226f223a3530317d0a7b226f223a3530327d0a7b226f223a3530337d0a7b226f223a3530347d0a7b226f223a3530357d0a7b226f223a3530367d0a7b226f223a3530377d0a7b226f223a3530387d0a7b226f223a3530397d0a7b226f223a3531307d0a7b226f223a3531317d0a7b226f223a3531327d0a7b226f223a3531337d0a7b226f223a3531347d0a7b226f223a3531357d0a7b226f223a3531367d0a7b226f223a3531377d0a7b226f223a3531387d0a7b226f223a3531397d0a7b226f223a3532307d0a7b226f223a3532317d0a7b226f223a3532327d0a7b226f223a3532337d0a7b226f223a3532347d0a7b226f223a3532357d0a7b226f223a3532367d0a7b226f223a3532377d0a7b226f223a3532387d0a7b226f223a3532397d0a7b226f223a3533307d0a7b226f223a3533317d0a7b226f223a3533327d0a7b226f223a3533337d0a7b226f223a3533347d0a7b226f223a3533357d0a7b226f223a3533367d0a7b226f223a3533377d0a7b226f223a3533387d0a7b226f223a3533397d0a7b226f223a3534307d0a7b226f223a3534317d0a7b226f223a3534327d0a7b226f223a3534337d0a7b226f223a3534347d0a7b226f223a3534357d0a7b226f223a3534367d0a7b226f223a3534377d0a7b226f223a3534387d0a7b226f223a3534397d0a7b226f223a3535307d0a7b226f223a3535317d0a7b226f223a3535327d0a7b226f223a3535337d0a7b226f223a3535347d0a7b226f223a3535357d0a7b226f223a3535367d0a7b226f223a3535377d0a7b226f223a3535387d0a7b226f223a3535397d0a7b226f223a3536307d0a7b226f223a3536317d0a7b226f223a3536327d0a7b226f223a3536337d0a7b226f223a3536347d0a7b226f223a3536357d0a7b226f223a3536367d0a7b226f223a3536377d0a7b226f223a3536387d0a7b226f223a3536397d0a7b226f223a3537307d0a7b226f223a3537317d0a7b226f223a3537327d0a7b226f223a3537337d0a7b226f223a3537347d0a7b226f223a3537357d0a7b226f223a3537367d0a7b226f223a3537377d0a7b226f223a3537387d0a7b226f223a3537397d0a7b226f223a3538307d0a7b226f223a3538317d0a7b226f223a3538327d0a7b226f223a3538337d0a7b226f223a3538347d0a7b226f223a3538357d0a7b226f223a3538367d0a7b226f223a3538377d0a7b226f223a3538387d0a7b226f223a3538397d0a7b226f223a3539307d0a7b226f223a3539317d0a7b226f223a3539327d0a7b226f223a3539337d0a7b226f223a3539347d0a7b226f223a3539357d0a7b226f223a3539367d0a7b226f223a3539377d0a7b226f223a3539387d0a7b226f223a3539397d0a7b226f223a3630307d0a7b226f223a3630317d0a7b226f223a3630327d0a7b226f223a3630337d0a7b226f223a3630347d0a7b226f223a3630357d0a7b226f223a3630367d0a7b226f223a3630377d0a7b226f223a3630387d0a7b226f223a3630397d0a7b226f223a3631307d0a7b226f223a3631317d0a7b226f223a3631327d0a7b226f223a3631337d0a7b226f223a3631347d0a7b226f223a3631357d0a7b226f223a3631367d0a7b226f223a3631377d0a7b226f223a3631387d0a7b226f223a3631397d0a7b226f223a3632307d0a7b226f223a3632317d0a7b226f223a3632327d0a7b226f223a3632337d0a7b226f223a3632347d0a7b226f223a3632357d0a7b226f223a3632367d0a7b226f223a3632377d0a7b226f223a3632387d0a7b226f223a3632397d0a7b226f223a3633307d0a7b226f223a3633317d0a7b226f223a3633327d0a7b226f223a3633337d0a7b226f223a3633347d0a7b226f223a3633357d0a7b226f223a3633367d0a7b226f223a3633377d0a7b226f223a3633387d0a7b226f223a3633397d0a7b226f223a3634307d0a7b226f223a3634317d0a7b226f223a3634327d0a7b226f223a3634337d0a7b226f223a3634347d0a7b226f223a3634357d0a7b226f223a3634367d0a7b226f223a3634377d0a7b226f223a3634387d0a7b226f223a3634397d0a7b226f223a3635307d0a7b226f223a3635317d0a7b226f223a3635327d0a7b226f223a3635337d0a7b226f223a3635347d0a7b226f223a3635357d0a7b226f223a3635367d0a7b226f223a3635377d0a7b226f223a3635387d0a7b226f223a3635397d0a7b226f223a3636307d0a7b226f223a3636317d0a7b226f223a3636327d0a7b226f223a3636337d0a7b226f223a3636347d0a7b226f223a3636357d0a7b226f223a3636367d0a7b226f223a3636377d0a7b226f223a3636387d0a7b226f223a3636397d0a7b226f223a3637307d0a7b226f223a3637317d0a7b226f223a3637327d0a7b226f223a3637337d0a7b226f223a3637347d0a7b226f223a3637357d0a7b226f223a3637367d0a7b226f223a3637377d0a7b226f223a3637387d0a7b226f223a3637397d0a7b226f223a3638307d0a7b226f223a3638317d0a7b226f223a3638327d0a7b226f223a3638337d0a7b226f223a3638347d0a7b226f223a3638357d0a7b226f223a3638367d0a7b226f223a3638377d0a7b226f223a3638387d0a7b226f223a3638397d0a7b226f223a3639307d0a7b226f223a3639317d0a7b226f223a3639327d0a7b226f223a3639337d0a7b226f223a3639347d0a7b226f223a3639357d0a7b226f223a3639367d0a7b226f223a3639377d0a7b226f223a3639387d0a7b226f223a3639397d0a7b226f223a3730307d0a7b226f223a3730317d0a7b226f223a3730327d0a7b226f223a3730337d0a7b226f223a3730347d0a7b226f223a3730357d0a7b226f223a3730367d0a7b226f223a3730377d0a7b226f223a3730387d0a7b226f223a3730397d0a7b226f223a3731307d0a7b226f223a3731317d0a7b226f223a3731327d0a7b226f223a3731337d0a7b226f223a3731347d0a7b226f223a3731357d0a7b226f223a3731367d0a7b226f223a3731377d0a7b226f223a3731387d0a7b226f223a3731397d0a7b226f223a3732307d0a7b226f223a3732317d0a7b226f223a3732327d0a7b226f223a3732337d0a7b226f223a3732347d0a7b226f223a3732357d0a7b226f223a3732367d0a7b226f223a3732377d0a7b226f223a3732387d0a7b226f223a3732397d0a7b226f223a3733307d0a7b226f223a3733317d0a7b226f223a3733327d0a7b226f223a3733337d0a7b226f223a3733347d0a7b226f223a3733357d0a7b226f223a3733367d0a7b226f223a3733377d0a7b226f223a3733387d0a7b226f223a3733397d0a7b226f223a3734307d0a7b226f223a3734317d0a7b226f223a3734327d0a7b226f223a3734337d0a7b226f223a3734347d0a7b226f223a3734357d0a7b226f223a3734367d0a7b226f223a3734377d0a7b226f223a3734387d0a7b226f223a3734397d0a7b226f223a3735307d0a7b226f223a3735317d0a7b226f223a3735327d0a7b226f223a3735337d0a7b226f223a3735347d0a7b226f223a3735357d0a7b226f223a3735367d0a7b226f223a3735377d0a7b226f223a3735387d0a7b226f223a3735397d0a7b226f223a3736307d0a7b226f223a3736317d0a7b226f223a3736327d0a7b226f223a3736337d0a7b226f223a3736347d0a7b226f223a3736357d0a7b226f223a3736367d0a7b226f223a3736377d0a7b226f223a3736387d0a7b226f223a3736397d0a7b226f223a3737307d0a7b226f223a3737317d0a7b226f223a3737327d0a7b226f223a3737337d0a7b226f223a3737347d0a7b226f223a3737357d0a7b226f223a3737367d0a7b226f223a3737377d0a7b226f223a3737387d0a7b226f223a3737397d0a7b226f223a3738307d0a7b226f223a3738317d0a7b226f223a3738327d0a7b226f223a3738337d0a7b226f223a3738347d0a7b226f223a3738357d0a7b226f223a3738367d0a7b226f223a3738377d0a7b226f223a3738387d0a7b226f223a3738397d0a7b226f223a3739307d0a7b226f223a3739317d0a7b226f223a3739327d0a7b226f223a3739337d0a7b226f223a3739347d0a7b226f223a3739357d0a7b226f223a3739367d0a7b226f223a3739377d0a7b226f223a3739387d0a7b226f223a3739397d0a7b226f223a3830307d0a7b226f223a3830317d0a7b226f223a3830327d0a7b226f223a3830337d0a7b226f223a3830347d0a7b226f223a3830357d0a7b226f223a3830367d0a7b226f223a3830377d0a7b226f223a3830387d0a7b226f223a3830397d0a7b226f223a3831307d0a7b226f223a3831317d0a7b226f223a3831327d0a7b226f223a3831337d0a7b226f223a3831347d0a7b226f223a3831357d0a7b226f223a3831367d0a7b226f223a3831377d0a7b226f223a3831387d0a7b226f223a3831397d0a7b226f223a3832307d0a7b226f223a3832317d0a7b226f223a3832327d0a7b226f223a3832337d0a7b226f223a3832347d0a7b226f223a3832357d0a7b226f223a3832367d0a7b226f223a3832377d0a7b226f223a3832387d0a7b226f223a3832397d0a7b226f223a3833307d0a7b226f223a3833317d0a7b226f223a3833327d0a7b226f223a3833337d0a7b226f223a3833347d0a7b226f223a3833357d0a7b226f223a3833367d0a7b226f223a3833377d0a7b226f223a3833387d0a7b226f223a3833397d0a7b226f223a3834307d0a7b226f223a3834317d0a7b226f223a3834327d0a7b226f223a3834337d0a7b226f223a3834347d0a7b226f223a3834357d0a7b226f223a3834367d0a7b226f223a3834377d0a7b226f223a3834387d0a7b226f223a3834397d0a7b226f223a3835307d0a7b226f223a3835317d0a7b226f223a3835327d0a7b226f223a3835337d0a7b226f223a3835347d0a7b226f223a3835357d0a7b226f223a3835367d0a7b226f223a3835377d0a7b226f223a3835387d0a7b226f223a3835397d0a7b226f223a3836307d0a7b226f223a3836317d0a7b226f223a3836327d0a7b226f223a3836337d0a7b226f223a3836347d0a7b226f223a3836357d0a7b226f223a3836367d0a7b226f223a3836377d0a7b226f223a3836387d0a7b226f223a3836397d0a7b226f223a3837307d0a7b226f223a3837317d0a7b226f223a3837327d0a7b226f223a3837337d0a7b226f223a3837347d0a7b226f223a3837357d0a7b226f223a3837367d0a7b226f223a3837377d0a7b226f223a3837387d0a7b226f223a3837397d0a7b226f223a3838307d0a7b226f223a3838317d0a7b226f223a3838327d0a7b226f223a3838337d0a7b226f223a3838347d0a7b226f223a3838357d0a7b226f223a3838367d0a7b226f223a3838377d0a7b226f223a3838387d0a7b226f223a3838397d0a7b226f223a3839307d0a7b226f223a3839317d0a7b226f223a3839327d0a7b226f223a3839337d0a7b226f223a3839347d0a7b226f223a3839357d0a7b226f223a3839367d0a7b226f223a3839377d0a7b226f223a3839387d0a7b226f223a3839397d0a7b226f223a3930307d0a7b226f223a3930317d0a7b226f223a3930327d0a7b226f223a3930337d0a7b226f223a3930347d0a7b226f223a3930357d0a7b226f223a3930367d0a7b226f223a3930377d0a7b226f223a3930387d0a7b226f223a3930397d0a7b226f223a3931307d0a7b226f223a3931317d0a7b226f223a3931327d0a7b226f223a3931337d0a7b226f223a3931347d0a7b226f223a3931357d0a7b226f223a3931367d0a7b226f223a3931377d0a7b226f223a3931387d0a7b226f223a3931397d0a7b226f223a3932307d0a7b226f223a3932317d0a7b226f223a3932327d0a7b226f223a3932337d0a7b226f223a3932347d0a7b226f223a3932357d0a7b226f223a3932367d0a7b226f223a3932377d0a7b226f223a3932387d0a7b226f223a3932397d0a7b226f223a3933307d0a7b226f223a3933317d0a7b226f223a3933327d0a7b226f223a3933337d0a7b226f223a3933347d0a7b226f223a3933357d0a7b226f223a3933367d0a7b226f223a3933377d0a7b226f223a3933387d0a7b226f223a3933397d0a7b226f223a3934307d0a7b226f223a3934317d0a7b226f223a3934327d0a7b226f223a3934337d0a7b226f223a3934347d0a7b226f223a3934357d0a7b226f223a3934367d0a7b226f223a3934377d0a7b226f223a3934387d0a7b226f223a3934397d0a7b226f223a3935307d0a7b226f223a3935317d0a7b226f223a3935327d0a7b226f223a3935337d0a7b226f223a3935347d0a7b226f223a3935357d0a7b226f223a3935367d0a7b226f223a3935377d0a7b226f223a3935387d0a7b226f223a3935397d0a7b226f223a3936307d0a7b226f223a3936317d0a7b226f223a3936327d0a7b226f223a3936337d0a7b226f223a3936347d0a7b226f223a3936357d0a7b226f223a3936367d0a7b226f223a3936377d0a7b226f223a3936387d0a7b226f223a3936397d0a7b226f223a3937307d0a7b226f223a3937317d0a7b226f223a3937327d0a7b226f223a3937337d0a7b226f223a3937347d0a7b226f223a3937357d0a7b226f223a3937367d0a7b226f223a3937377d0a7b226f223a3937387d0a7b226f223a3937397d0a7b226f223a3938307d0a7b226f223a3938317d0a7b226f223a3938327d0a7b226f223a3938337d0a7b226f223a3938347d0a7b226f223a3938357d0a7b226f223a3938367d0a7b226f223a3938377d0a7b226f223a3938387d0a7b226f223a3938397d0a7b226f223a3939307d0a7b226f223a3939317d0a7b226f223a3939327d0a7b226f223a3939337d0a7b226f223a3939347d0a7b226f223a3939357d0a7b226f223a3939367d0a7b226f223a3939377d0a7b226f223a3939387d0a7b226f223a3939397d0a7b226f223a313030307d0a","obligation_id":"robl_interop","sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277"}, + {"account_id":"acct_interop","body_hex":"7b2261223a322c22636f6e74726f6c223a225c75303030375c6e5c225c5c222c226e616d65223a22636166c3a920f09f988020e697a5e69cace8aa9e222c22736570223a22e280a8222c227a223a317d0a7b22666c6f6174223a302e33303030303030303030303030303030342c226d61785f696e74223a393030373139393235343734303939312c226e223a302c226e6567223a2d31322e352c226e6573746564223a7b2261223a5b5d2c227a223a5b312c7b2261223a747275652c2262223a6e756c6c7d5d7d2c2274696e79223a31652d377d0a","obligation_id":"robl_interop","sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}, + {"account_id":"acct_interop","body_hex":"7b2264617465223a22323032362d31302d3031222c22696d7072657373696f6e73223a313230302c226d656469615f6275795f6964223a226d625f31222c227061636b6167655f6964223a22706b675f31222c227370656e64223a2234322e3137227d0a","obligation_id":"robl_interop","sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"} ], "objects": { - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/261f3a1eb893ee2ab06dcba59c4c226e643150f9151fd3136aa79daac7c2a33a/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz": "H4sIAAAAAAAAA03azbEcJxSA0b3CUAQD3B9wUg5ApdxdrnocZtW777rkdwYa+s/vf3//8/n768//z/HznD/P9fOMn2f+POvn2T/P/fM8tyN4i+Mmx22OGx23Om523O644XHL85an/9Zbnrc8b3ne8rzlecvzluctr1tet7z8M9zyuuV1y+uW1y2vW163HLcctxy3HP6FbzluOW45bjluOW45bzlvOW85bzn9z7vlvOW85bzlvOW65brluuW65brl8ndxy3XLdct1y33Lfct9y33Lfct9y+1P7pb7lvuW9y3vW963vG953/K+5X3L21/zLe9bPrd8bvnc8rnlc8vnls8tn1s+oDwpqHxY+cDyoeWDy4eXDzAfYj7IfMz44mjGA/lEPpLP5EP5VD6WXA4wx3zmzWBzwDnoHHgOPgegg9CB6GB0rPfDYgamg9MB6iB1oDpYHbAOWgeuI96vlxnEDmQHswPaQe3AdnA7wB3kjnw/kWbAO+gd+A5+B8CD4IHwYHhAPOr9DpvB8QB5kDxQHiwPmAfNA+fB8+j3Y28G0oPpAfWgemA9uB5gD7IH2mO/FcUMugfeg+8B+CB8ID4YH5APysd5y9ZbtyxcnE/OJ+eT88n55HxyPjmfnM/xFkczOJ+cT84n55PzyfnkfL719y3AXyuwGW8NfovwW4XfMvzW4bcQcz45n5zP9ZZ5MzifnE/OJ+eT88n55HxyPjmf8fYSZnA+OZ+cT84n55PzyfnkfHI+821YzOB8cj45n5xPzifnk/PJ+eR81tsVmcH55HxyPjmfnE/OJ+eT88n57Lf1MoPzyfnkfHI+OZ+cT84n55Pzud/+zgzOJ+eT88n55HxyPjmfnE/O53mbyLeLtI3kfHG+OF+cL84X54vzxfnifI23VTWD88X54nxxvjhfnC/OF+eL8zXfftgMzhfni/PF+eJ8cb7ejvttud+e+2vTbcbbdr9999t4v53323pzvjhfnC/OV7ydvRmcL84X54vzxfnifHG+OF+cr3yvD2ZwvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDiP9479XrLfW/Z7zf56zzbjvWm/V+33rv1etjkPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dzfqdo7Vnvnau9g7Z2sfR2tmfEO197p2jte4zw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+S8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L83rn6O8g/Z2kv6P0d5b+DtO/TtPNeOfp70Cd8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDhvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/O+92cvauzd3f2Ls/e7dm7Pnv3Z18XaGa8KzTOm/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+c73dX/i7L3235uy5/9+XvwvzdmL8r8687czM435xvzjfnm/PN+eZ8c74535xvzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/n530d8z6Ped/HvA9k3hcy7xOZ943M+0jmfSXz9ZnM13cyn7+//gMn4sd0rSYAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/705459c9b7547dedbf7cabf978d9ea55759f2358d3e062257529da157ffbea9e/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz": "H4sIAAAAAAAAA1WOwQ7BQBRF975C7vqRN1Vp5v2KERk1RFJTYZqgadK/YCGsJIi/8CcWYukThJ27Omd1bgkLiQhp7sMizyAwBTMnxhsYA4K3MwdBasf3S/N93NXN5/b03N9e1wMISzeH4FGfQdhAVNUoMc5yGyDc7vD/YsLMrgZTHyCaOVFaR904iVlrRfAQJng3gbRU1O5+eRncCFJ+P/b6v0JP0U/DonCEIcQXWVb1K0KY+jVEuVZSNT7kPqqA1gAAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/7f7820795b5d933eebf3dce8962b626072b809c407fed39552dad3cf51cc2b1a/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz": "H4sIAAAAAAAAAyXKQQqAIBBG4X3H+NcZMxIFXkYsB5GwJGsR0d3DWr6Pd8O7Q2CgSQ+KSRGjRUx5l1LithYY1kQtkvjo7HReNnoYpMnWMbt5cUF+y0v4sGRZa/e64xFP8wJBEPOlZAAAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/c5866668a6a52204b0b829245568ead9e74a674d056c8ae7949d8f140f50ee40/b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8/000000.jsonl.gz": "H4sIAAAAAAAAAyXKQQqAIBBG4X3H+NcZMxIFXkYsB5GwJGsR0d3DWr6Pd8O7Q2CgSQ+KSRGjRUx5l1LithYY1kQtkvjo7HReNnoYpMnWMbt5cUF+y0v4sGRZa/e64xFP8wJBEPOlZAAAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/e0d86fc58a94fda3c36f8454d332c4988cd985c83f5ce0cf7d90fc57ce9f7de7/d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f/000000.jsonl.gz": "H4sIAAAAAAAAA03azbEcJxSA0b3CUAQD3B9wUg5ApdxdrnocZtW777rkdwYa+s/vf3//8/n768//z/HznD/P9fOMn2f+POvn2T/P/fM8tyN4i+Mmx22OGx23Om523O644XHL85an/9Zbnrc8b3ne8rzlecvzluctr1tet7z8M9zyuuV1y+uW1y2vW163HLcctxy3HP6FbzluOW45bjluOW45bzlvOW85bzn9z7vlvOW85bzlvOW65brluuW65brl8ndxy3XLdct1y33Lfct9y33Lfct9y+1P7pb7lvuW9y3vW963vG953/K+5X3L21/zLe9bPrd8bvnc8rnlc8vnls8tn1s+oDwpqHxY+cDyoeWDy4eXDzAfYj7IfMz44mjGA/lEPpLP5EP5VD6WXA4wx3zmzWBzwDnoHHgOPgegg9CB6GB0rPfDYgamg9MB6iB1oDpYHbAOWgeuI96vlxnEDmQHswPaQe3AdnA7wB3kjnw/kWbAO+gd+A5+B8CD4IHwYHhAPOr9DpvB8QB5kDxQHiwPmAfNA+fB8+j3Y28G0oPpAfWgemA9uB5gD7IH2mO/FcUMugfeg+8B+CB8ID4YH5APysd5y9ZbtyxcnE/OJ+eT88n55HxyPjmfnM/xFkczOJ+cT84n55PzyfnkfL719y3AXyuwGW8NfovwW4XfMvzW4bcQcz45n5zP9ZZ5MzifnE/OJ+eT88n55HxyPjmf8fYSZnA+OZ+cT84n55PzyfnkfHI+821YzOB8cj45n5xPzifnk/PJ+eR81tsVmcH55HxyPjmfnE/OJ+eT88n57Lf1MoPzyfnkfHI+OZ+cT84n55Pzud/+zgzOJ+eT88n55HxyPjmfnE/O53mbyLeLtI3kfHG+OF+cL84X54vzxfnifI23VTWD88X54nxxvjhfnC/OF+eL8zXfftgMzhfni/PF+eJ8cb7ejvttud+e+2vTbcbbdr9999t4v53323pzvjhfnC/OV7ydvRmcL84X54vzxfnifHG+OF+cr3yvD2ZwvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDiP9479XrLfW/Z7zf56zzbjvWm/V+33rv1etjkPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dzfqdo7Vnvnau9g7Z2sfR2tmfEO197p2jte4zw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+S8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L83rn6O8g/Z2kv6P0d5b+DtO/TtPNeOfp70Cd8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDhvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/O+92cvauzd3f2Ls/e7dm7Pnv3Z18XaGa8KzTOm/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+c73dX/i7L3235uy5/9+XvwvzdmL8r8687czM435xvzjfnm/PN+eZ8c74535xvzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/n530d8z6Ped/HvA9k3hcy7xOZ943M+0jmfSXz9ZnM13cyn7+//gMn4sd0rSYAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/ff289a3d78dbb114f4d8a549013c3e2aef591b1b1ee0bee597d6f466428bf7dc/20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd/000000.jsonl.gz": "H4sIAAAAAAAAA1WOwQ7BQBRF975C7vqRN1Vp5v2KERk1RFJTYZqgadK/YCGsJIi/8CcWYukThJ27Omd1bgkLiQhp7sMizyAwBTMnxhsYA4K3MwdBasf3S/N93NXN5/b03N9e1wMISzeH4FGfQdhAVNUoMc5yGyDc7vD/YsLMrgZTHyCaOVFaR904iVlrRfAQJng3gbRU1O5+eRncCFJ+P/b6v0JP0U/DonCEIcQXWVb1K0KY+jVEuVZSNT7kPqqA1gAAAA==" + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/261f3a1eb893ee2ab06dcba59c4c226e643150f9151fd3136aa79daac7c2a33a/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz":"H4sIAAAAAAAAA03azbEcJxSA0b3CUAQD3B9wUg5ApdxdrnocZtW777rkdwYa+s/vf3//8/n768//z/HznD/P9fOMn2f+POvn2T/P/fM8tyN4i+Mmx22OGx23Om523O644XHL85an/9Zbnrc8b3ne8rzlecvzluctr1tet7z8M9zyuuV1y+uW1y2vW163HLcctxy3HP6FbzluOW45bjluOW45bzlvOW85bzn9z7vlvOW85bzlvOW65brluuW65brl8ndxy3XLdct1y33Lfct9y33Lfct9y+1P7pb7lvuW9y3vW963vG953/K+5X3L21/zLe9bPrd8bvnc8rnlc8vnls8tn1s+oDwpqHxY+cDyoeWDy4eXDzAfYj7IfMz44mjGA/lEPpLP5EP5VD6WXA4wx3zmzWBzwDnoHHgOPgegg9CB6GB0rPfDYgamg9MB6iB1oDpYHbAOWgeuI96vlxnEDmQHswPaQe3AdnA7wB3kjnw/kWbAO+gd+A5+B8CD4IHwYHhAPOr9DpvB8QB5kDxQHiwPmAfNA+fB8+j3Y28G0oPpAfWgemA9uB5gD7IH2mO/FcUMugfeg+8B+CB8ID4YH5APysd5y9ZbtyxcnE/OJ+eT88n55HxyPjmfnM/xFkczOJ+cT84n55PzyfnkfL719y3AXyuwGW8NfovwW4XfMvzW4bcQcz45n5zP9ZZ5MzifnE/OJ+eT88n55HxyPjmf8fYSZnA+OZ+cT84n55PzyfnkfHI+821YzOB8cj45n5xPzifnk/PJ+eR81tsVmcH55HxyPjmfnE/OJ+eT88n57Lf1MoPzyfnkfHI+OZ+cT84n55Pzud/+zgzOJ+eT88n55HxyPjmfnE/O53mbyLeLtI3kfHG+OF+cL84X54vzxfnifI23VTWD88X54nxxvjhfnC/OF+eL8zXfftgMzhfni/PF+eJ8cb7ejvttud+e+2vTbcbbdr9999t4v53323pzvjhfnC/OV7ydvRmcL84X54vzxfnifHG+OF+cr3yvD2ZwvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDiP9479XrLfW/Z7zf56zzbjvWm/V+33rv1etjkPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dzfqdo7Vnvnau9g7Z2sfR2tmfEO197p2jte4zw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+S8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L83rn6O8g/Z2kv6P0d5b+DtO/TtPNeOfp70Cd8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDhvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/O+92cvauzd3f2Ls/e7dm7Pnv3Z18XaGa8KzTOm/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+c73dX/i7L3235uy5/9+XvwvzdmL8r8687czM435xvzjfnm/PN+eZ8c74535xvzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/n530d8z6Ped/HvA9k3hcy7xOZ943M+0jmfSXz9ZnM13cyn7+//gMn4sd0rSYAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/705459c9b7547dedbf7cabf978d9ea55759f2358d3e062257529da157ffbea9e/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz":"H4sIAAAAAAAAA1WOwQ7BQBRF975C7vqRN1Vp5v2KERk1RFJTYZqgadK/YCGsJIi/8CcWYukThJ27Omd1bgkLiQhp7sMizyAwBTMnxhsYA4K3MwdBasf3S/N93NXN5/b03N9e1wMISzeH4FGfQdhAVNUoMc5yGyDc7vD/YsLMrgZTHyCaOVFaR904iVlrRfAQJng3gbRU1O5+eRncCFJ+P/b6v0JP0U/DonCEIcQXWVb1K0KY+jVEuVZSNT7kPqqA1gAAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/7f7820795b5d933eebf3dce8962b626072b809c407fed39552dad3cf51cc2b1a/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz":"H4sIAAAAAAAAAyXKQQqAIBBG4X3H+NcZMxIFXkYsB5GwJGsR0d3DWr6Pd8O7Q2CgSQ+KSRGjRUx5l1LithYY1kQtkvjo7HReNnoYpMnWMbt5cUF+y0v4sGRZa/e64xFP8wJBEPOlZAAAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/c5866668a6a52204b0b829245568ead9e74a674d056c8ae7949d8f140f50ee40/b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8/000000.jsonl.gz":"H4sIAAAAAAAAAyXKQQqAIBBG4X3H+NcZMxIFXkYsB5GwJGsR0d3DWr6Pd8O7Q2CgSQ+KSRGjRUx5l1LithYY1kQtkvjo7HReNnoYpMnWMbt5cUF+y0v4sGRZa/e64xFP8wJBEPOlZAAAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/e0d86fc58a94fda3c36f8454d332c4988cd985c83f5ce0cf7d90fc57ce9f7de7/d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f/000000.jsonl.gz":"H4sIAAAAAAAAA03azbEcJxSA0b3CUAQD3B9wUg5ApdxdrnocZtW777rkdwYa+s/vf3//8/n768//z/HznD/P9fOMn2f+POvn2T/P/fM8tyN4i+Mmx22OGx23Om523O644XHL85an/9Zbnrc8b3ne8rzlecvzluctr1tet7z8M9zyuuV1y+uW1y2vW163HLcctxy3HP6FbzluOW45bjluOW45bzlvOW85bzn9z7vlvOW85bzlvOW65brluuW65brl8ndxy3XLdct1y33Lfct9y33Lfct9y+1P7pb7lvuW9y3vW963vG953/K+5X3L21/zLe9bPrd8bvnc8rnlc8vnls8tn1s+oDwpqHxY+cDyoeWDy4eXDzAfYj7IfMz44mjGA/lEPpLP5EP5VD6WXA4wx3zmzWBzwDnoHHgOPgegg9CB6GB0rPfDYgamg9MB6iB1oDpYHbAOWgeuI96vlxnEDmQHswPaQe3AdnA7wB3kjnw/kWbAO+gd+A5+B8CD4IHwYHhAPOr9DpvB8QB5kDxQHiwPmAfNA+fB8+j3Y28G0oPpAfWgemA9uB5gD7IH2mO/FcUMugfeg+8B+CB8ID4YH5APysd5y9ZbtyxcnE/OJ+eT88n55HxyPjmfnM/xFkczOJ+cT84n55PzyfnkfL719y3AXyuwGW8NfovwW4XfMvzW4bcQcz45n5zP9ZZ5MzifnE/OJ+eT88n55HxyPjmf8fYSZnA+OZ+cT84n55PzyfnkfHI+821YzOB8cj45n5xPzifnk/PJ+eR81tsVmcH55HxyPjmfnE/OJ+eT88n57Lf1MoPzyfnkfHI+OZ+cT84n55Pzud/+zgzOJ+eT88n55HxyPjmfnE/O53mbyLeLtI3kfHG+OF+cL84X54vzxfnifI23VTWD88X54nxxvjhfnC/OF+eL8zXfftgMzhfni/PF+eJ8cb7ejvttud+e+2vTbcbbdr9999t4v53323pzvjhfnC/OV7ydvRmcL84X54vzxfnifHG+OF+cr3yvD2ZwvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/OF+eL88X54nxxvjhfnC/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDiP9479XrLfW/Z7zf56zzbjvWm/V+33rv1etjkPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz4Dw4D86D8+A8OA/Og/PgPDgPzoPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dzfqdo7Vnvnau9g7Z2sfR2tmfEO197p2jte4zw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+Q8OU/Ok/PkPDlPzpPz5Dw5T86T8+S8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L83rn6O8g/Z2kv6P0d5b+DtO/TtPNeOfp70Cd8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDgvzovz4rw4L86L8+K8OC/Oi/PivDhvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b86b8+a8OW/O+92cvauzd3f2Ls/e7dm7Pnv3Z18XaGa8KzTOm/PmvDlvzpvz5rw5b86b8+a8OW/Om/PmvDlvzpvz5rw5b84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+c73dX/i7L3235uy5/9+XvwvzdmL8r8687czM435xvzjfnm/PN+eZ8c74535xvzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/n530d8z6Ped/HvA9k3hcy7xOZ943M+0jmfSXz9ZnM13cyn7+//gMn4sd0rSYAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/ff289a3d78dbb114f4d8a549013c3e2aef591b1b1ee0bee597d6f466428bf7dc/20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd/000000.jsonl.gz":"H4sIAAAAAAAAA1WOwQ7BQBRF975C7vqRN1Vp5v2KERk1RFJTYZqgadK/YCGsJIi/8CcWYukThJ27Omd1bgkLiQhp7sMizyAwBTMnxhsYA4K3MwdBasf3S/N93NXN5/b03N9e1wMISzeH4FGfQdhAVNUoMc5yGyDc7vD/YsLMrgZTHyCaOVFaR904iVlrRfAQJng3gbRU1O5+eRncCFJ+P/b6v0JP0U/DonCEIcQXWVb1K0KY+jVEuVZSNT7kPqqA1gAAAA==" }, "row_chunks": [ - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1, - "row_set_id": "rrev_js_single-row_adjustment_inline", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/7f7820795b5d933eebf3dce8962b626072b809c407fed39552dad3cf51cc2b1a/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "4d6f55b54163962ef467b8cd2ae7751f75abf8d64d347f44197c4d504ecd6091", - "row_count": 1, - "row_set_id": "rrev_js_single-row_adjustment_reports", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": 106 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1, - "row_set_id": "rrev_js_single-row_revision_inline", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/c5866668a6a52204b0b829245568ead9e74a674d056c8ae7949d8f140f50ee40/b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "4d6f55b54163962ef467b8cd2ae7751f75abf8d64d347f44197c4d504ecd6091", - "row_count": 1, - "row_set_id": "rrev_js_single-row_revision_reports", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": 106 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1001, - "row_set_id": "rrev_js_three-segments_adjustment_inline", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/261f3a1eb893ee2ab06dcba59c4c226e643150f9151fd3136aa79daac7c2a33a/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "aad958a59f3b31b3ec2e54f6bb02f0ac70e19f19a349816a6d943b24d9c5b484", - "row_count": 1001, - "row_set_id": "rrev_js_three-segments_adjustment_reports", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": 1987 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1001, - "row_set_id": "rrev_js_three-segments_revision_inline", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/e0d86fc58a94fda3c36f8454d332c4988cd985c83f5ce0cf7d90fc57ce9f7de7/d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "aad958a59f3b31b3ec2e54f6bb02f0ac70e19f19a349816a6d943b24d9c5b484", - "row_count": 1001, - "row_set_id": "rrev_js_three-segments_revision_reports", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": 1987 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 2, - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_inline", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/705459c9b7547dedbf7cabf978d9ea55759f2358d3e062257529da157ffbea9e/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "28a23f11b0bd66046049d747ccf769ccea2179aa7b8815f1d225d6594bb140e5", - "row_count": 2, - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_reports", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": 196 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 2, - "row_set_id": "rrev_js_unicode-and-numbers_revision_inline", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/ff289a3d78dbb114f4d8a549013c3e2aef591b1b1ee0bee597d6f466428bf7dc/20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "28a23f11b0bd66046049d747ccf769ccea2179aa7b8815f1d225d6594bb140e5", - "row_count": 2, - "row_set_id": "rrev_js_unicode-and-numbers_revision_reports", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": 196 - } + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1,"row_set_id":"rrev_js_single-row_adjustment_inline","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/7f7820795b5d933eebf3dce8962b626072b809c407fed39552dad3cf51cc2b1a/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"4d6f55b54163962ef467b8cd2ae7751f75abf8d64d347f44197c4d504ecd6091","row_count":1,"row_set_id":"rrev_js_single-row_adjustment_reports","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":106}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1,"row_set_id":"rrev_js_single-row_revision_inline","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/c5866668a6a52204b0b829245568ead9e74a674d056c8ae7949d8f140f50ee40/b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"4d6f55b54163962ef467b8cd2ae7751f75abf8d64d347f44197c4d504ecd6091","row_count":1,"row_set_id":"rrev_js_single-row_revision_reports","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":106}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1001,"row_set_id":"rrev_js_three-segments_adjustment_inline","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/261f3a1eb893ee2ab06dcba59c4c226e643150f9151fd3136aa79daac7c2a33a/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"aad958a59f3b31b3ec2e54f6bb02f0ac70e19f19a349816a6d943b24d9c5b484","row_count":1001,"row_set_id":"rrev_js_three-segments_adjustment_reports","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":1987}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1001,"row_set_id":"rrev_js_three-segments_revision_inline","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/e0d86fc58a94fda3c36f8454d332c4988cd985c83f5ce0cf7d90fc57ce9f7de7/d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"aad958a59f3b31b3ec2e54f6bb02f0ac70e19f19a349816a6d943b24d9c5b484","row_count":1001,"row_set_id":"rrev_js_three-segments_revision_reports","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":1987}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":2,"row_set_id":"rrev_js_unicode-and-numbers_adjustment_inline","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/705459c9b7547dedbf7cabf978d9ea55759f2358d3e062257529da157ffbea9e/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"28a23f11b0bd66046049d747ccf769ccea2179aa7b8815f1d225d6594bb140e5","row_count":2,"row_set_id":"rrev_js_unicode-and-numbers_adjustment_reports","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":196}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":2,"row_set_id":"rrev_js_unicode-and-numbers_revision_inline","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/ff289a3d78dbb114f4d8a549013c3e2aef591b1b1ee0bee597d6f466428bf7dc/20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"28a23f11b0bd66046049d747ccf769ccea2179aa7b8815f1d225d6594bb140e5","row_count":2,"row_set_id":"rrev_js_unicode-and-numbers_revision_reports","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":196} ], "row_sets": [ - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_js_single-row_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_js_single-row_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "69403a2a705dcce0c564849c26c55a899f0c1158dd12a56ce370b004069fd5fa", - "content_size_bytes": 344, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_js_single-row_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8", - "content_size_bytes": 345, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_js_single-row_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_js_three-segments_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_js_three-segments_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "de89268832ff75b1fe6f77a61de3c174ed49289172bde20d537b10c4b067fbc0", - "content_size_bytes": 10152, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_js_three-segments_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f", - "content_size_bytes": 10153, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_js_three-segments_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_js_unicode-and-numbers_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "39d5233af55a871656ea98bc512f3c6bc3446ff30a97747f26cd117913af83af", - "content_size_bytes": 467, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_js_unicode-and-numbers_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd", - "content_size_bytes": 468, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_js_unicode-and-numbers_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - } + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_js_single-row_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_js_single-row_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"69403a2a705dcce0c564849c26c55a899f0c1158dd12a56ce370b004069fd5fa","content_size_bytes":344,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_js_single-row_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"b5367a164b48f0c0e31537bda570038687db83416988840fc2bacd96300ad3e8","content_size_bytes":345,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_js_single-row_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_js_three-segments_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_js_three-segments_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"de89268832ff75b1fe6f77a61de3c174ed49289172bde20d537b10c4b067fbc0","content_size_bytes":10152,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_js_three-segments_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"d1ca264c9c51c4f7146f5f9b791a05fffee44cc55eca090dcc73ec826522874f","content_size_bytes":10153,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_js_three-segments_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_js_unicode-and-numbers_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_js_unicode-and-numbers_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"39d5233af55a871656ea98bc512f3c6bc3446ff30a97747f26cd117913af83af","content_size_bytes":467,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_js_unicode-and-numbers_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"20391f076cea7702c50fa4ce949705afca0529fd3de4787ac66dbbafe448b6cd","content_size_bytes":468,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_js_unicode-and-numbers_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null} ] } } diff --git a/tests/fixtures/reporting-row-storage-interop-v1/python-written.json b/tests/fixtures/reporting-row-storage-interop-v1/python-written.json index 9a83b1400..465adcf34 100644 --- a/tests/fixtures/reporting-row-storage-interop-v1/python-written.json +++ b/tests/fixtures/reporting-row-storage-interop-v1/python-written.json @@ -1,12926 +1,59 @@ { "expected": [ - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "f04cd965936bf4821f54148bcaa037a7a9b4f0315e7d9cead0b6e90baed2f818", - "content_size_bytes": 344, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_single-row_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8", - "content_size_bytes": 345, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_single-row_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_single-row_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_single-row_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "date": "2026-10-01", - "impressions": 1200, - "media_buy_id": "mb_1", - "package_id": "pkg_1", - "spend": "42.17" - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "795bc2f0c2979e4e3178d994692f994f34220bca84af68db33705ac6fd52efdd", - "content_size_bytes": 467, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_unicode-and-numbers_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869", - "content_size_bytes": 468, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_unicode-and-numbers_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "a": 2, - "control": "\u0007\n\"\\", - "name": "caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e", - "sep": "\u2028", - "z": 1 - }, - { - "float": 0.30000000000000004, - "max_int": 9007199254740991, - "n": 0, - "neg": -12.5, - "nested": { - "a": [], - "z": [ - 1, - { - "a": true, - "b": null - } - ] - }, - "tiny": 1e-07 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "cd76d91d96b99808a64c0a9348d81d82584153af22ea74d1138970381705f57d", - "content_size_bytes": 10152, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_three-segments_revision_inline", - "row_set_kind": "revision", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b", - "content_size_bytes": 10153, - "control_totals": [ - { - "name": "impressions", - "value": "1200", - "value_type": "integer" - }, - { - "name": "spend", - "unit": "USD", - "value": "42.17", - "value_type": "decimal" - } - ], - "digest_profile": "revision_envelope_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_three-segments_revision_reports", - "row_set_kind": "revision", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "inline", - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_three-segments_adjustment_inline", - "row_set_kind": "adjustment", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - }, - { - "account_id": "acct_interop", - "binding_id": "reports", - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "control_totals": null, - "digest_profile": "rows_v1", - "obligation_id": "robl_interop", - "row_set_id": "rrev_py_three-segments_adjustment_reports", - "row_set_kind": "adjustment", - "rows": [ - { - "o": 0 - }, - { - "o": 1 - }, - { - "o": 2 - }, - { - "o": 3 - }, - { - "o": 4 - }, - { - "o": 5 - }, - { - "o": 6 - }, - { - "o": 7 - }, - { - "o": 8 - }, - { - "o": 9 - }, - { - "o": 10 - }, - { - "o": 11 - }, - { - "o": 12 - }, - { - "o": 13 - }, - { - "o": 14 - }, - { - "o": 15 - }, - { - "o": 16 - }, - { - "o": 17 - }, - { - "o": 18 - }, - { - "o": 19 - }, - { - "o": 20 - }, - { - "o": 21 - }, - { - "o": 22 - }, - { - "o": 23 - }, - { - "o": 24 - }, - { - "o": 25 - }, - { - "o": 26 - }, - { - "o": 27 - }, - { - "o": 28 - }, - { - "o": 29 - }, - { - "o": 30 - }, - { - "o": 31 - }, - { - "o": 32 - }, - { - "o": 33 - }, - { - "o": 34 - }, - { - "o": 35 - }, - { - "o": 36 - }, - { - "o": 37 - }, - { - "o": 38 - }, - { - "o": 39 - }, - { - "o": 40 - }, - { - "o": 41 - }, - { - "o": 42 - }, - { - "o": 43 - }, - { - "o": 44 - }, - { - "o": 45 - }, - { - "o": 46 - }, - { - "o": 47 - }, - { - "o": 48 - }, - { - "o": 49 - }, - { - "o": 50 - }, - { - "o": 51 - }, - { - "o": 52 - }, - { - "o": 53 - }, - { - "o": 54 - }, - { - "o": 55 - }, - { - "o": 56 - }, - { - "o": 57 - }, - { - "o": 58 - }, - { - "o": 59 - }, - { - "o": 60 - }, - { - "o": 61 - }, - { - "o": 62 - }, - { - "o": 63 - }, - { - "o": 64 - }, - { - "o": 65 - }, - { - "o": 66 - }, - { - "o": 67 - }, - { - "o": 68 - }, - { - "o": 69 - }, - { - "o": 70 - }, - { - "o": 71 - }, - { - "o": 72 - }, - { - "o": 73 - }, - { - "o": 74 - }, - { - "o": 75 - }, - { - "o": 76 - }, - { - "o": 77 - }, - { - "o": 78 - }, - { - "o": 79 - }, - { - "o": 80 - }, - { - "o": 81 - }, - { - "o": 82 - }, - { - "o": 83 - }, - { - "o": 84 - }, - { - "o": 85 - }, - { - "o": 86 - }, - { - "o": 87 - }, - { - "o": 88 - }, - { - "o": 89 - }, - { - "o": 90 - }, - { - "o": 91 - }, - { - "o": 92 - }, - { - "o": 93 - }, - { - "o": 94 - }, - { - "o": 95 - }, - { - "o": 96 - }, - { - "o": 97 - }, - { - "o": 98 - }, - { - "o": 99 - }, - { - "o": 100 - }, - { - "o": 101 - }, - { - "o": 102 - }, - { - "o": 103 - }, - { - "o": 104 - }, - { - "o": 105 - }, - { - "o": 106 - }, - { - "o": 107 - }, - { - "o": 108 - }, - { - "o": 109 - }, - { - "o": 110 - }, - { - "o": 111 - }, - { - "o": 112 - }, - { - "o": 113 - }, - { - "o": 114 - }, - { - "o": 115 - }, - { - "o": 116 - }, - { - "o": 117 - }, - { - "o": 118 - }, - { - "o": 119 - }, - { - "o": 120 - }, - { - "o": 121 - }, - { - "o": 122 - }, - { - "o": 123 - }, - { - "o": 124 - }, - { - "o": 125 - }, - { - "o": 126 - }, - { - "o": 127 - }, - { - "o": 128 - }, - { - "o": 129 - }, - { - "o": 130 - }, - { - "o": 131 - }, - { - "o": 132 - }, - { - "o": 133 - }, - { - "o": 134 - }, - { - "o": 135 - }, - { - "o": 136 - }, - { - "o": 137 - }, - { - "o": 138 - }, - { - "o": 139 - }, - { - "o": 140 - }, - { - "o": 141 - }, - { - "o": 142 - }, - { - "o": 143 - }, - { - "o": 144 - }, - { - "o": 145 - }, - { - "o": 146 - }, - { - "o": 147 - }, - { - "o": 148 - }, - { - "o": 149 - }, - { - "o": 150 - }, - { - "o": 151 - }, - { - "o": 152 - }, - { - "o": 153 - }, - { - "o": 154 - }, - { - "o": 155 - }, - { - "o": 156 - }, - { - "o": 157 - }, - { - "o": 158 - }, - { - "o": 159 - }, - { - "o": 160 - }, - { - "o": 161 - }, - { - "o": 162 - }, - { - "o": 163 - }, - { - "o": 164 - }, - { - "o": 165 - }, - { - "o": 166 - }, - { - "o": 167 - }, - { - "o": 168 - }, - { - "o": 169 - }, - { - "o": 170 - }, - { - "o": 171 - }, - { - "o": 172 - }, - { - "o": 173 - }, - { - "o": 174 - }, - { - "o": 175 - }, - { - "o": 176 - }, - { - "o": 177 - }, - { - "o": 178 - }, - { - "o": 179 - }, - { - "o": 180 - }, - { - "o": 181 - }, - { - "o": 182 - }, - { - "o": 183 - }, - { - "o": 184 - }, - { - "o": 185 - }, - { - "o": 186 - }, - { - "o": 187 - }, - { - "o": 188 - }, - { - "o": 189 - }, - { - "o": 190 - }, - { - "o": 191 - }, - { - "o": 192 - }, - { - "o": 193 - }, - { - "o": 194 - }, - { - "o": 195 - }, - { - "o": 196 - }, - { - "o": 197 - }, - { - "o": 198 - }, - { - "o": 199 - }, - { - "o": 200 - }, - { - "o": 201 - }, - { - "o": 202 - }, - { - "o": 203 - }, - { - "o": 204 - }, - { - "o": 205 - }, - { - "o": 206 - }, - { - "o": 207 - }, - { - "o": 208 - }, - { - "o": 209 - }, - { - "o": 210 - }, - { - "o": 211 - }, - { - "o": 212 - }, - { - "o": 213 - }, - { - "o": 214 - }, - { - "o": 215 - }, - { - "o": 216 - }, - { - "o": 217 - }, - { - "o": 218 - }, - { - "o": 219 - }, - { - "o": 220 - }, - { - "o": 221 - }, - { - "o": 222 - }, - { - "o": 223 - }, - { - "o": 224 - }, - { - "o": 225 - }, - { - "o": 226 - }, - { - "o": 227 - }, - { - "o": 228 - }, - { - "o": 229 - }, - { - "o": 230 - }, - { - "o": 231 - }, - { - "o": 232 - }, - { - "o": 233 - }, - { - "o": 234 - }, - { - "o": 235 - }, - { - "o": 236 - }, - { - "o": 237 - }, - { - "o": 238 - }, - { - "o": 239 - }, - { - "o": 240 - }, - { - "o": 241 - }, - { - "o": 242 - }, - { - "o": 243 - }, - { - "o": 244 - }, - { - "o": 245 - }, - { - "o": 246 - }, - { - "o": 247 - }, - { - "o": 248 - }, - { - "o": 249 - }, - { - "o": 250 - }, - { - "o": 251 - }, - { - "o": 252 - }, - { - "o": 253 - }, - { - "o": 254 - }, - { - "o": 255 - }, - { - "o": 256 - }, - { - "o": 257 - }, - { - "o": 258 - }, - { - "o": 259 - }, - { - "o": 260 - }, - { - "o": 261 - }, - { - "o": 262 - }, - { - "o": 263 - }, - { - "o": 264 - }, - { - "o": 265 - }, - { - "o": 266 - }, - { - "o": 267 - }, - { - "o": 268 - }, - { - "o": 269 - }, - { - "o": 270 - }, - { - "o": 271 - }, - { - "o": 272 - }, - { - "o": 273 - }, - { - "o": 274 - }, - { - "o": 275 - }, - { - "o": 276 - }, - { - "o": 277 - }, - { - "o": 278 - }, - { - "o": 279 - }, - { - "o": 280 - }, - { - "o": 281 - }, - { - "o": 282 - }, - { - "o": 283 - }, - { - "o": 284 - }, - { - "o": 285 - }, - { - "o": 286 - }, - { - "o": 287 - }, - { - "o": 288 - }, - { - "o": 289 - }, - { - "o": 290 - }, - { - "o": 291 - }, - { - "o": 292 - }, - { - "o": 293 - }, - { - "o": 294 - }, - { - "o": 295 - }, - { - "o": 296 - }, - { - "o": 297 - }, - { - "o": 298 - }, - { - "o": 299 - }, - { - "o": 300 - }, - { - "o": 301 - }, - { - "o": 302 - }, - { - "o": 303 - }, - { - "o": 304 - }, - { - "o": 305 - }, - { - "o": 306 - }, - { - "o": 307 - }, - { - "o": 308 - }, - { - "o": 309 - }, - { - "o": 310 - }, - { - "o": 311 - }, - { - "o": 312 - }, - { - "o": 313 - }, - { - "o": 314 - }, - { - "o": 315 - }, - { - "o": 316 - }, - { - "o": 317 - }, - { - "o": 318 - }, - { - "o": 319 - }, - { - "o": 320 - }, - { - "o": 321 - }, - { - "o": 322 - }, - { - "o": 323 - }, - { - "o": 324 - }, - { - "o": 325 - }, - { - "o": 326 - }, - { - "o": 327 - }, - { - "o": 328 - }, - { - "o": 329 - }, - { - "o": 330 - }, - { - "o": 331 - }, - { - "o": 332 - }, - { - "o": 333 - }, - { - "o": 334 - }, - { - "o": 335 - }, - { - "o": 336 - }, - { - "o": 337 - }, - { - "o": 338 - }, - { - "o": 339 - }, - { - "o": 340 - }, - { - "o": 341 - }, - { - "o": 342 - }, - { - "o": 343 - }, - { - "o": 344 - }, - { - "o": 345 - }, - { - "o": 346 - }, - { - "o": 347 - }, - { - "o": 348 - }, - { - "o": 349 - }, - { - "o": 350 - }, - { - "o": 351 - }, - { - "o": 352 - }, - { - "o": 353 - }, - { - "o": 354 - }, - { - "o": 355 - }, - { - "o": 356 - }, - { - "o": 357 - }, - { - "o": 358 - }, - { - "o": 359 - }, - { - "o": 360 - }, - { - "o": 361 - }, - { - "o": 362 - }, - { - "o": 363 - }, - { - "o": 364 - }, - { - "o": 365 - }, - { - "o": 366 - }, - { - "o": 367 - }, - { - "o": 368 - }, - { - "o": 369 - }, - { - "o": 370 - }, - { - "o": 371 - }, - { - "o": 372 - }, - { - "o": 373 - }, - { - "o": 374 - }, - { - "o": 375 - }, - { - "o": 376 - }, - { - "o": 377 - }, - { - "o": 378 - }, - { - "o": 379 - }, - { - "o": 380 - }, - { - "o": 381 - }, - { - "o": 382 - }, - { - "o": 383 - }, - { - "o": 384 - }, - { - "o": 385 - }, - { - "o": 386 - }, - { - "o": 387 - }, - { - "o": 388 - }, - { - "o": 389 - }, - { - "o": 390 - }, - { - "o": 391 - }, - { - "o": 392 - }, - { - "o": 393 - }, - { - "o": 394 - }, - { - "o": 395 - }, - { - "o": 396 - }, - { - "o": 397 - }, - { - "o": 398 - }, - { - "o": 399 - }, - { - "o": 400 - }, - { - "o": 401 - }, - { - "o": 402 - }, - { - "o": 403 - }, - { - "o": 404 - }, - { - "o": 405 - }, - { - "o": 406 - }, - { - "o": 407 - }, - { - "o": 408 - }, - { - "o": 409 - }, - { - "o": 410 - }, - { - "o": 411 - }, - { - "o": 412 - }, - { - "o": 413 - }, - { - "o": 414 - }, - { - "o": 415 - }, - { - "o": 416 - }, - { - "o": 417 - }, - { - "o": 418 - }, - { - "o": 419 - }, - { - "o": 420 - }, - { - "o": 421 - }, - { - "o": 422 - }, - { - "o": 423 - }, - { - "o": 424 - }, - { - "o": 425 - }, - { - "o": 426 - }, - { - "o": 427 - }, - { - "o": 428 - }, - { - "o": 429 - }, - { - "o": 430 - }, - { - "o": 431 - }, - { - "o": 432 - }, - { - "o": 433 - }, - { - "o": 434 - }, - { - "o": 435 - }, - { - "o": 436 - }, - { - "o": 437 - }, - { - "o": 438 - }, - { - "o": 439 - }, - { - "o": 440 - }, - { - "o": 441 - }, - { - "o": 442 - }, - { - "o": 443 - }, - { - "o": 444 - }, - { - "o": 445 - }, - { - "o": 446 - }, - { - "o": 447 - }, - { - "o": 448 - }, - { - "o": 449 - }, - { - "o": 450 - }, - { - "o": 451 - }, - { - "o": 452 - }, - { - "o": 453 - }, - { - "o": 454 - }, - { - "o": 455 - }, - { - "o": 456 - }, - { - "o": 457 - }, - { - "o": 458 - }, - { - "o": 459 - }, - { - "o": 460 - }, - { - "o": 461 - }, - { - "o": 462 - }, - { - "o": 463 - }, - { - "o": 464 - }, - { - "o": 465 - }, - { - "o": 466 - }, - { - "o": 467 - }, - { - "o": 468 - }, - { - "o": 469 - }, - { - "o": 470 - }, - { - "o": 471 - }, - { - "o": 472 - }, - { - "o": 473 - }, - { - "o": 474 - }, - { - "o": 475 - }, - { - "o": 476 - }, - { - "o": 477 - }, - { - "o": 478 - }, - { - "o": 479 - }, - { - "o": 480 - }, - { - "o": 481 - }, - { - "o": 482 - }, - { - "o": 483 - }, - { - "o": 484 - }, - { - "o": 485 - }, - { - "o": 486 - }, - { - "o": 487 - }, - { - "o": 488 - }, - { - "o": 489 - }, - { - "o": 490 - }, - { - "o": 491 - }, - { - "o": 492 - }, - { - "o": 493 - }, - { - "o": 494 - }, - { - "o": 495 - }, - { - "o": 496 - }, - { - "o": 497 - }, - { - "o": 498 - }, - { - "o": 499 - }, - { - "o": 500 - }, - { - "o": 501 - }, - { - "o": 502 - }, - { - "o": 503 - }, - { - "o": 504 - }, - { - "o": 505 - }, - { - "o": 506 - }, - { - "o": 507 - }, - { - "o": 508 - }, - { - "o": 509 - }, - { - "o": 510 - }, - { - "o": 511 - }, - { - "o": 512 - }, - { - "o": 513 - }, - { - "o": 514 - }, - { - "o": 515 - }, - { - "o": 516 - }, - { - "o": 517 - }, - { - "o": 518 - }, - { - "o": 519 - }, - { - "o": 520 - }, - { - "o": 521 - }, - { - "o": 522 - }, - { - "o": 523 - }, - { - "o": 524 - }, - { - "o": 525 - }, - { - "o": 526 - }, - { - "o": 527 - }, - { - "o": 528 - }, - { - "o": 529 - }, - { - "o": 530 - }, - { - "o": 531 - }, - { - "o": 532 - }, - { - "o": 533 - }, - { - "o": 534 - }, - { - "o": 535 - }, - { - "o": 536 - }, - { - "o": 537 - }, - { - "o": 538 - }, - { - "o": 539 - }, - { - "o": 540 - }, - { - "o": 541 - }, - { - "o": 542 - }, - { - "o": 543 - }, - { - "o": 544 - }, - { - "o": 545 - }, - { - "o": 546 - }, - { - "o": 547 - }, - { - "o": 548 - }, - { - "o": 549 - }, - { - "o": 550 - }, - { - "o": 551 - }, - { - "o": 552 - }, - { - "o": 553 - }, - { - "o": 554 - }, - { - "o": 555 - }, - { - "o": 556 - }, - { - "o": 557 - }, - { - "o": 558 - }, - { - "o": 559 - }, - { - "o": 560 - }, - { - "o": 561 - }, - { - "o": 562 - }, - { - "o": 563 - }, - { - "o": 564 - }, - { - "o": 565 - }, - { - "o": 566 - }, - { - "o": 567 - }, - { - "o": 568 - }, - { - "o": 569 - }, - { - "o": 570 - }, - { - "o": 571 - }, - { - "o": 572 - }, - { - "o": 573 - }, - { - "o": 574 - }, - { - "o": 575 - }, - { - "o": 576 - }, - { - "o": 577 - }, - { - "o": 578 - }, - { - "o": 579 - }, - { - "o": 580 - }, - { - "o": 581 - }, - { - "o": 582 - }, - { - "o": 583 - }, - { - "o": 584 - }, - { - "o": 585 - }, - { - "o": 586 - }, - { - "o": 587 - }, - { - "o": 588 - }, - { - "o": 589 - }, - { - "o": 590 - }, - { - "o": 591 - }, - { - "o": 592 - }, - { - "o": 593 - }, - { - "o": 594 - }, - { - "o": 595 - }, - { - "o": 596 - }, - { - "o": 597 - }, - { - "o": 598 - }, - { - "o": 599 - }, - { - "o": 600 - }, - { - "o": 601 - }, - { - "o": 602 - }, - { - "o": 603 - }, - { - "o": 604 - }, - { - "o": 605 - }, - { - "o": 606 - }, - { - "o": 607 - }, - { - "o": 608 - }, - { - "o": 609 - }, - { - "o": 610 - }, - { - "o": 611 - }, - { - "o": 612 - }, - { - "o": 613 - }, - { - "o": 614 - }, - { - "o": 615 - }, - { - "o": 616 - }, - { - "o": 617 - }, - { - "o": 618 - }, - { - "o": 619 - }, - { - "o": 620 - }, - { - "o": 621 - }, - { - "o": 622 - }, - { - "o": 623 - }, - { - "o": 624 - }, - { - "o": 625 - }, - { - "o": 626 - }, - { - "o": 627 - }, - { - "o": 628 - }, - { - "o": 629 - }, - { - "o": 630 - }, - { - "o": 631 - }, - { - "o": 632 - }, - { - "o": 633 - }, - { - "o": 634 - }, - { - "o": 635 - }, - { - "o": 636 - }, - { - "o": 637 - }, - { - "o": 638 - }, - { - "o": 639 - }, - { - "o": 640 - }, - { - "o": 641 - }, - { - "o": 642 - }, - { - "o": 643 - }, - { - "o": 644 - }, - { - "o": 645 - }, - { - "o": 646 - }, - { - "o": 647 - }, - { - "o": 648 - }, - { - "o": 649 - }, - { - "o": 650 - }, - { - "o": 651 - }, - { - "o": 652 - }, - { - "o": 653 - }, - { - "o": 654 - }, - { - "o": 655 - }, - { - "o": 656 - }, - { - "o": 657 - }, - { - "o": 658 - }, - { - "o": 659 - }, - { - "o": 660 - }, - { - "o": 661 - }, - { - "o": 662 - }, - { - "o": 663 - }, - { - "o": 664 - }, - { - "o": 665 - }, - { - "o": 666 - }, - { - "o": 667 - }, - { - "o": 668 - }, - { - "o": 669 - }, - { - "o": 670 - }, - { - "o": 671 - }, - { - "o": 672 - }, - { - "o": 673 - }, - { - "o": 674 - }, - { - "o": 675 - }, - { - "o": 676 - }, - { - "o": 677 - }, - { - "o": 678 - }, - { - "o": 679 - }, - { - "o": 680 - }, - { - "o": 681 - }, - { - "o": 682 - }, - { - "o": 683 - }, - { - "o": 684 - }, - { - "o": 685 - }, - { - "o": 686 - }, - { - "o": 687 - }, - { - "o": 688 - }, - { - "o": 689 - }, - { - "o": 690 - }, - { - "o": 691 - }, - { - "o": 692 - }, - { - "o": 693 - }, - { - "o": 694 - }, - { - "o": 695 - }, - { - "o": 696 - }, - { - "o": 697 - }, - { - "o": 698 - }, - { - "o": 699 - }, - { - "o": 700 - }, - { - "o": 701 - }, - { - "o": 702 - }, - { - "o": 703 - }, - { - "o": 704 - }, - { - "o": 705 - }, - { - "o": 706 - }, - { - "o": 707 - }, - { - "o": 708 - }, - { - "o": 709 - }, - { - "o": 710 - }, - { - "o": 711 - }, - { - "o": 712 - }, - { - "o": 713 - }, - { - "o": 714 - }, - { - "o": 715 - }, - { - "o": 716 - }, - { - "o": 717 - }, - { - "o": 718 - }, - { - "o": 719 - }, - { - "o": 720 - }, - { - "o": 721 - }, - { - "o": 722 - }, - { - "o": 723 - }, - { - "o": 724 - }, - { - "o": 725 - }, - { - "o": 726 - }, - { - "o": 727 - }, - { - "o": 728 - }, - { - "o": 729 - }, - { - "o": 730 - }, - { - "o": 731 - }, - { - "o": 732 - }, - { - "o": 733 - }, - { - "o": 734 - }, - { - "o": 735 - }, - { - "o": 736 - }, - { - "o": 737 - }, - { - "o": 738 - }, - { - "o": 739 - }, - { - "o": 740 - }, - { - "o": 741 - }, - { - "o": 742 - }, - { - "o": 743 - }, - { - "o": 744 - }, - { - "o": 745 - }, - { - "o": 746 - }, - { - "o": 747 - }, - { - "o": 748 - }, - { - "o": 749 - }, - { - "o": 750 - }, - { - "o": 751 - }, - { - "o": 752 - }, - { - "o": 753 - }, - { - "o": 754 - }, - { - "o": 755 - }, - { - "o": 756 - }, - { - "o": 757 - }, - { - "o": 758 - }, - { - "o": 759 - }, - { - "o": 760 - }, - { - "o": 761 - }, - { - "o": 762 - }, - { - "o": 763 - }, - { - "o": 764 - }, - { - "o": 765 - }, - { - "o": 766 - }, - { - "o": 767 - }, - { - "o": 768 - }, - { - "o": 769 - }, - { - "o": 770 - }, - { - "o": 771 - }, - { - "o": 772 - }, - { - "o": 773 - }, - { - "o": 774 - }, - { - "o": 775 - }, - { - "o": 776 - }, - { - "o": 777 - }, - { - "o": 778 - }, - { - "o": 779 - }, - { - "o": 780 - }, - { - "o": 781 - }, - { - "o": 782 - }, - { - "o": 783 - }, - { - "o": 784 - }, - { - "o": 785 - }, - { - "o": 786 - }, - { - "o": 787 - }, - { - "o": 788 - }, - { - "o": 789 - }, - { - "o": 790 - }, - { - "o": 791 - }, - { - "o": 792 - }, - { - "o": 793 - }, - { - "o": 794 - }, - { - "o": 795 - }, - { - "o": 796 - }, - { - "o": 797 - }, - { - "o": 798 - }, - { - "o": 799 - }, - { - "o": 800 - }, - { - "o": 801 - }, - { - "o": 802 - }, - { - "o": 803 - }, - { - "o": 804 - }, - { - "o": 805 - }, - { - "o": 806 - }, - { - "o": 807 - }, - { - "o": 808 - }, - { - "o": 809 - }, - { - "o": 810 - }, - { - "o": 811 - }, - { - "o": 812 - }, - { - "o": 813 - }, - { - "o": 814 - }, - { - "o": 815 - }, - { - "o": 816 - }, - { - "o": 817 - }, - { - "o": 818 - }, - { - "o": 819 - }, - { - "o": 820 - }, - { - "o": 821 - }, - { - "o": 822 - }, - { - "o": 823 - }, - { - "o": 824 - }, - { - "o": 825 - }, - { - "o": 826 - }, - { - "o": 827 - }, - { - "o": 828 - }, - { - "o": 829 - }, - { - "o": 830 - }, - { - "o": 831 - }, - { - "o": 832 - }, - { - "o": 833 - }, - { - "o": 834 - }, - { - "o": 835 - }, - { - "o": 836 - }, - { - "o": 837 - }, - { - "o": 838 - }, - { - "o": 839 - }, - { - "o": 840 - }, - { - "o": 841 - }, - { - "o": 842 - }, - { - "o": 843 - }, - { - "o": 844 - }, - { - "o": 845 - }, - { - "o": 846 - }, - { - "o": 847 - }, - { - "o": 848 - }, - { - "o": 849 - }, - { - "o": 850 - }, - { - "o": 851 - }, - { - "o": 852 - }, - { - "o": 853 - }, - { - "o": 854 - }, - { - "o": 855 - }, - { - "o": 856 - }, - { - "o": 857 - }, - { - "o": 858 - }, - { - "o": 859 - }, - { - "o": 860 - }, - { - "o": 861 - }, - { - "o": 862 - }, - { - "o": 863 - }, - { - "o": 864 - }, - { - "o": 865 - }, - { - "o": 866 - }, - { - "o": 867 - }, - { - "o": 868 - }, - { - "o": 869 - }, - { - "o": 870 - }, - { - "o": 871 - }, - { - "o": 872 - }, - { - "o": 873 - }, - { - "o": 874 - }, - { - "o": 875 - }, - { - "o": 876 - }, - { - "o": 877 - }, - { - "o": 878 - }, - { - "o": 879 - }, - { - "o": 880 - }, - { - "o": 881 - }, - { - "o": 882 - }, - { - "o": 883 - }, - { - "o": 884 - }, - { - "o": 885 - }, - { - "o": 886 - }, - { - "o": 887 - }, - { - "o": 888 - }, - { - "o": 889 - }, - { - "o": 890 - }, - { - "o": 891 - }, - { - "o": 892 - }, - { - "o": 893 - }, - { - "o": 894 - }, - { - "o": 895 - }, - { - "o": 896 - }, - { - "o": 897 - }, - { - "o": 898 - }, - { - "o": 899 - }, - { - "o": 900 - }, - { - "o": 901 - }, - { - "o": 902 - }, - { - "o": 903 - }, - { - "o": 904 - }, - { - "o": 905 - }, - { - "o": 906 - }, - { - "o": 907 - }, - { - "o": 908 - }, - { - "o": 909 - }, - { - "o": 910 - }, - { - "o": 911 - }, - { - "o": 912 - }, - { - "o": 913 - }, - { - "o": 914 - }, - { - "o": 915 - }, - { - "o": 916 - }, - { - "o": 917 - }, - { - "o": 918 - }, - { - "o": 919 - }, - { - "o": 920 - }, - { - "o": 921 - }, - { - "o": 922 - }, - { - "o": 923 - }, - { - "o": 924 - }, - { - "o": 925 - }, - { - "o": 926 - }, - { - "o": 927 - }, - { - "o": 928 - }, - { - "o": 929 - }, - { - "o": 930 - }, - { - "o": 931 - }, - { - "o": 932 - }, - { - "o": 933 - }, - { - "o": 934 - }, - { - "o": 935 - }, - { - "o": 936 - }, - { - "o": 937 - }, - { - "o": 938 - }, - { - "o": 939 - }, - { - "o": 940 - }, - { - "o": 941 - }, - { - "o": 942 - }, - { - "o": 943 - }, - { - "o": 944 - }, - { - "o": 945 - }, - { - "o": 946 - }, - { - "o": 947 - }, - { - "o": 948 - }, - { - "o": 949 - }, - { - "o": 950 - }, - { - "o": 951 - }, - { - "o": 952 - }, - { - "o": 953 - }, - { - "o": 954 - }, - { - "o": 955 - }, - { - "o": 956 - }, - { - "o": 957 - }, - { - "o": 958 - }, - { - "o": 959 - }, - { - "o": 960 - }, - { - "o": 961 - }, - { - "o": 962 - }, - { - "o": 963 - }, - { - "o": 964 - }, - { - "o": 965 - }, - { - "o": 966 - }, - { - "o": 967 - }, - { - "o": 968 - }, - { - "o": 969 - }, - { - "o": 970 - }, - { - "o": 971 - }, - { - "o": 972 - }, - { - "o": 973 - }, - { - "o": 974 - }, - { - "o": 975 - }, - { - "o": 976 - }, - { - "o": 977 - }, - { - "o": 978 - }, - { - "o": 979 - }, - { - "o": 980 - }, - { - "o": 981 - }, - { - "o": 982 - }, - { - "o": 983 - }, - { - "o": 984 - }, - { - "o": 985 - }, - { - "o": 986 - }, - { - "o": 987 - }, - { - "o": 988 - }, - { - "o": 989 - }, - { - "o": 990 - }, - { - "o": 991 - }, - { - "o": 992 - }, - { - "o": 993 - }, - { - "o": 994 - }, - { - "o": 995 - }, - { - "o": 996 - }, - { - "o": 997 - }, - { - "o": 998 - }, - { - "o": 999 - }, - { - "o": 1000 - } - ] - } + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"f04cd965936bf4821f54148bcaa037a7a9b4f0315e7d9cead0b6e90baed2f818","content_size_bytes":344,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_single-row_revision_inline","row_set_kind":"revision","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8","content_size_bytes":345,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_single-row_revision_reports","row_set_kind":"revision","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_single-row_adjustment_inline","row_set_kind":"adjustment","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_single-row_adjustment_reports","row_set_kind":"adjustment","rows":[{"date":"2026-10-01","impressions":1200,"media_buy_id":"mb_1","package_id":"pkg_1","spend":"42.17"}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"795bc2f0c2979e4e3178d994692f994f34220bca84af68db33705ac6fd52efdd","content_size_bytes":467,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_unicode-and-numbers_revision_inline","row_set_kind":"revision","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869","content_size_bytes":468,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_unicode-and-numbers_revision_reports","row_set_kind":"revision","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_unicode-and-numbers_adjustment_inline","row_set_kind":"adjustment","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_unicode-and-numbers_adjustment_reports","row_set_kind":"adjustment","rows":[{"a":2,"control":"\u0007\n\"\\","name":"caf\u00e9 \ud83d\ude00 \u65e5\u672c\u8a9e","sep":"\u2028","z":1},{"float":0.30000000000000004,"max_int":9007199254740991,"n":0,"neg":-12.5,"nested":{"a":[],"z":[1,{"a":true,"b":null}]},"tiny":1e-07}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"cd76d91d96b99808a64c0a9348d81d82584153af22ea74d1138970381705f57d","content_size_bytes":10152,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_three-segments_revision_inline","row_set_kind":"revision","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b","content_size_bytes":10153,"control_totals":[{"name":"impressions","value":"1200","value_type":"integer"},{"name":"spend","unit":"USD","value":"42.17","value_type":"decimal"}],"digest_profile":"revision_envelope_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_three-segments_revision_reports","row_set_kind":"revision","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"inline","content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_three-segments_adjustment_inline","row_set_kind":"adjustment","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]}, + {"account_id":"acct_interop","binding_id":"reports","content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"control_totals":null,"digest_profile":"rows_v1","obligation_id":"robl_interop","row_set_id":"rrev_py_three-segments_adjustment_reports","row_set_kind":"adjustment","rows":[{"o":0},{"o":1},{"o":2},{"o":3},{"o":4},{"o":5},{"o":6},{"o":7},{"o":8},{"o":9},{"o":10},{"o":11},{"o":12},{"o":13},{"o":14},{"o":15},{"o":16},{"o":17},{"o":18},{"o":19},{"o":20},{"o":21},{"o":22},{"o":23},{"o":24},{"o":25},{"o":26},{"o":27},{"o":28},{"o":29},{"o":30},{"o":31},{"o":32},{"o":33},{"o":34},{"o":35},{"o":36},{"o":37},{"o":38},{"o":39},{"o":40},{"o":41},{"o":42},{"o":43},{"o":44},{"o":45},{"o":46},{"o":47},{"o":48},{"o":49},{"o":50},{"o":51},{"o":52},{"o":53},{"o":54},{"o":55},{"o":56},{"o":57},{"o":58},{"o":59},{"o":60},{"o":61},{"o":62},{"o":63},{"o":64},{"o":65},{"o":66},{"o":67},{"o":68},{"o":69},{"o":70},{"o":71},{"o":72},{"o":73},{"o":74},{"o":75},{"o":76},{"o":77},{"o":78},{"o":79},{"o":80},{"o":81},{"o":82},{"o":83},{"o":84},{"o":85},{"o":86},{"o":87},{"o":88},{"o":89},{"o":90},{"o":91},{"o":92},{"o":93},{"o":94},{"o":95},{"o":96},{"o":97},{"o":98},{"o":99},{"o":100},{"o":101},{"o":102},{"o":103},{"o":104},{"o":105},{"o":106},{"o":107},{"o":108},{"o":109},{"o":110},{"o":111},{"o":112},{"o":113},{"o":114},{"o":115},{"o":116},{"o":117},{"o":118},{"o":119},{"o":120},{"o":121},{"o":122},{"o":123},{"o":124},{"o":125},{"o":126},{"o":127},{"o":128},{"o":129},{"o":130},{"o":131},{"o":132},{"o":133},{"o":134},{"o":135},{"o":136},{"o":137},{"o":138},{"o":139},{"o":140},{"o":141},{"o":142},{"o":143},{"o":144},{"o":145},{"o":146},{"o":147},{"o":148},{"o":149},{"o":150},{"o":151},{"o":152},{"o":153},{"o":154},{"o":155},{"o":156},{"o":157},{"o":158},{"o":159},{"o":160},{"o":161},{"o":162},{"o":163},{"o":164},{"o":165},{"o":166},{"o":167},{"o":168},{"o":169},{"o":170},{"o":171},{"o":172},{"o":173},{"o":174},{"o":175},{"o":176},{"o":177},{"o":178},{"o":179},{"o":180},{"o":181},{"o":182},{"o":183},{"o":184},{"o":185},{"o":186},{"o":187},{"o":188},{"o":189},{"o":190},{"o":191},{"o":192},{"o":193},{"o":194},{"o":195},{"o":196},{"o":197},{"o":198},{"o":199},{"o":200},{"o":201},{"o":202},{"o":203},{"o":204},{"o":205},{"o":206},{"o":207},{"o":208},{"o":209},{"o":210},{"o":211},{"o":212},{"o":213},{"o":214},{"o":215},{"o":216},{"o":217},{"o":218},{"o":219},{"o":220},{"o":221},{"o":222},{"o":223},{"o":224},{"o":225},{"o":226},{"o":227},{"o":228},{"o":229},{"o":230},{"o":231},{"o":232},{"o":233},{"o":234},{"o":235},{"o":236},{"o":237},{"o":238},{"o":239},{"o":240},{"o":241},{"o":242},{"o":243},{"o":244},{"o":245},{"o":246},{"o":247},{"o":248},{"o":249},{"o":250},{"o":251},{"o":252},{"o":253},{"o":254},{"o":255},{"o":256},{"o":257},{"o":258},{"o":259},{"o":260},{"o":261},{"o":262},{"o":263},{"o":264},{"o":265},{"o":266},{"o":267},{"o":268},{"o":269},{"o":270},{"o":271},{"o":272},{"o":273},{"o":274},{"o":275},{"o":276},{"o":277},{"o":278},{"o":279},{"o":280},{"o":281},{"o":282},{"o":283},{"o":284},{"o":285},{"o":286},{"o":287},{"o":288},{"o":289},{"o":290},{"o":291},{"o":292},{"o":293},{"o":294},{"o":295},{"o":296},{"o":297},{"o":298},{"o":299},{"o":300},{"o":301},{"o":302},{"o":303},{"o":304},{"o":305},{"o":306},{"o":307},{"o":308},{"o":309},{"o":310},{"o":311},{"o":312},{"o":313},{"o":314},{"o":315},{"o":316},{"o":317},{"o":318},{"o":319},{"o":320},{"o":321},{"o":322},{"o":323},{"o":324},{"o":325},{"o":326},{"o":327},{"o":328},{"o":329},{"o":330},{"o":331},{"o":332},{"o":333},{"o":334},{"o":335},{"o":336},{"o":337},{"o":338},{"o":339},{"o":340},{"o":341},{"o":342},{"o":343},{"o":344},{"o":345},{"o":346},{"o":347},{"o":348},{"o":349},{"o":350},{"o":351},{"o":352},{"o":353},{"o":354},{"o":355},{"o":356},{"o":357},{"o":358},{"o":359},{"o":360},{"o":361},{"o":362},{"o":363},{"o":364},{"o":365},{"o":366},{"o":367},{"o":368},{"o":369},{"o":370},{"o":371},{"o":372},{"o":373},{"o":374},{"o":375},{"o":376},{"o":377},{"o":378},{"o":379},{"o":380},{"o":381},{"o":382},{"o":383},{"o":384},{"o":385},{"o":386},{"o":387},{"o":388},{"o":389},{"o":390},{"o":391},{"o":392},{"o":393},{"o":394},{"o":395},{"o":396},{"o":397},{"o":398},{"o":399},{"o":400},{"o":401},{"o":402},{"o":403},{"o":404},{"o":405},{"o":406},{"o":407},{"o":408},{"o":409},{"o":410},{"o":411},{"o":412},{"o":413},{"o":414},{"o":415},{"o":416},{"o":417},{"o":418},{"o":419},{"o":420},{"o":421},{"o":422},{"o":423},{"o":424},{"o":425},{"o":426},{"o":427},{"o":428},{"o":429},{"o":430},{"o":431},{"o":432},{"o":433},{"o":434},{"o":435},{"o":436},{"o":437},{"o":438},{"o":439},{"o":440},{"o":441},{"o":442},{"o":443},{"o":444},{"o":445},{"o":446},{"o":447},{"o":448},{"o":449},{"o":450},{"o":451},{"o":452},{"o":453},{"o":454},{"o":455},{"o":456},{"o":457},{"o":458},{"o":459},{"o":460},{"o":461},{"o":462},{"o":463},{"o":464},{"o":465},{"o":466},{"o":467},{"o":468},{"o":469},{"o":470},{"o":471},{"o":472},{"o":473},{"o":474},{"o":475},{"o":476},{"o":477},{"o":478},{"o":479},{"o":480},{"o":481},{"o":482},{"o":483},{"o":484},{"o":485},{"o":486},{"o":487},{"o":488},{"o":489},{"o":490},{"o":491},{"o":492},{"o":493},{"o":494},{"o":495},{"o":496},{"o":497},{"o":498},{"o":499},{"o":500},{"o":501},{"o":502},{"o":503},{"o":504},{"o":505},{"o":506},{"o":507},{"o":508},{"o":509},{"o":510},{"o":511},{"o":512},{"o":513},{"o":514},{"o":515},{"o":516},{"o":517},{"o":518},{"o":519},{"o":520},{"o":521},{"o":522},{"o":523},{"o":524},{"o":525},{"o":526},{"o":527},{"o":528},{"o":529},{"o":530},{"o":531},{"o":532},{"o":533},{"o":534},{"o":535},{"o":536},{"o":537},{"o":538},{"o":539},{"o":540},{"o":541},{"o":542},{"o":543},{"o":544},{"o":545},{"o":546},{"o":547},{"o":548},{"o":549},{"o":550},{"o":551},{"o":552},{"o":553},{"o":554},{"o":555},{"o":556},{"o":557},{"o":558},{"o":559},{"o":560},{"o":561},{"o":562},{"o":563},{"o":564},{"o":565},{"o":566},{"o":567},{"o":568},{"o":569},{"o":570},{"o":571},{"o":572},{"o":573},{"o":574},{"o":575},{"o":576},{"o":577},{"o":578},{"o":579},{"o":580},{"o":581},{"o":582},{"o":583},{"o":584},{"o":585},{"o":586},{"o":587},{"o":588},{"o":589},{"o":590},{"o":591},{"o":592},{"o":593},{"o":594},{"o":595},{"o":596},{"o":597},{"o":598},{"o":599},{"o":600},{"o":601},{"o":602},{"o":603},{"o":604},{"o":605},{"o":606},{"o":607},{"o":608},{"o":609},{"o":610},{"o":611},{"o":612},{"o":613},{"o":614},{"o":615},{"o":616},{"o":617},{"o":618},{"o":619},{"o":620},{"o":621},{"o":622},{"o":623},{"o":624},{"o":625},{"o":626},{"o":627},{"o":628},{"o":629},{"o":630},{"o":631},{"o":632},{"o":633},{"o":634},{"o":635},{"o":636},{"o":637},{"o":638},{"o":639},{"o":640},{"o":641},{"o":642},{"o":643},{"o":644},{"o":645},{"o":646},{"o":647},{"o":648},{"o":649},{"o":650},{"o":651},{"o":652},{"o":653},{"o":654},{"o":655},{"o":656},{"o":657},{"o":658},{"o":659},{"o":660},{"o":661},{"o":662},{"o":663},{"o":664},{"o":665},{"o":666},{"o":667},{"o":668},{"o":669},{"o":670},{"o":671},{"o":672},{"o":673},{"o":674},{"o":675},{"o":676},{"o":677},{"o":678},{"o":679},{"o":680},{"o":681},{"o":682},{"o":683},{"o":684},{"o":685},{"o":686},{"o":687},{"o":688},{"o":689},{"o":690},{"o":691},{"o":692},{"o":693},{"o":694},{"o":695},{"o":696},{"o":697},{"o":698},{"o":699},{"o":700},{"o":701},{"o":702},{"o":703},{"o":704},{"o":705},{"o":706},{"o":707},{"o":708},{"o":709},{"o":710},{"o":711},{"o":712},{"o":713},{"o":714},{"o":715},{"o":716},{"o":717},{"o":718},{"o":719},{"o":720},{"o":721},{"o":722},{"o":723},{"o":724},{"o":725},{"o":726},{"o":727},{"o":728},{"o":729},{"o":730},{"o":731},{"o":732},{"o":733},{"o":734},{"o":735},{"o":736},{"o":737},{"o":738},{"o":739},{"o":740},{"o":741},{"o":742},{"o":743},{"o":744},{"o":745},{"o":746},{"o":747},{"o":748},{"o":749},{"o":750},{"o":751},{"o":752},{"o":753},{"o":754},{"o":755},{"o":756},{"o":757},{"o":758},{"o":759},{"o":760},{"o":761},{"o":762},{"o":763},{"o":764},{"o":765},{"o":766},{"o":767},{"o":768},{"o":769},{"o":770},{"o":771},{"o":772},{"o":773},{"o":774},{"o":775},{"o":776},{"o":777},{"o":778},{"o":779},{"o":780},{"o":781},{"o":782},{"o":783},{"o":784},{"o":785},{"o":786},{"o":787},{"o":788},{"o":789},{"o":790},{"o":791},{"o":792},{"o":793},{"o":794},{"o":795},{"o":796},{"o":797},{"o":798},{"o":799},{"o":800},{"o":801},{"o":802},{"o":803},{"o":804},{"o":805},{"o":806},{"o":807},{"o":808},{"o":809},{"o":810},{"o":811},{"o":812},{"o":813},{"o":814},{"o":815},{"o":816},{"o":817},{"o":818},{"o":819},{"o":820},{"o":821},{"o":822},{"o":823},{"o":824},{"o":825},{"o":826},{"o":827},{"o":828},{"o":829},{"o":830},{"o":831},{"o":832},{"o":833},{"o":834},{"o":835},{"o":836},{"o":837},{"o":838},{"o":839},{"o":840},{"o":841},{"o":842},{"o":843},{"o":844},{"o":845},{"o":846},{"o":847},{"o":848},{"o":849},{"o":850},{"o":851},{"o":852},{"o":853},{"o":854},{"o":855},{"o":856},{"o":857},{"o":858},{"o":859},{"o":860},{"o":861},{"o":862},{"o":863},{"o":864},{"o":865},{"o":866},{"o":867},{"o":868},{"o":869},{"o":870},{"o":871},{"o":872},{"o":873},{"o":874},{"o":875},{"o":876},{"o":877},{"o":878},{"o":879},{"o":880},{"o":881},{"o":882},{"o":883},{"o":884},{"o":885},{"o":886},{"o":887},{"o":888},{"o":889},{"o":890},{"o":891},{"o":892},{"o":893},{"o":894},{"o":895},{"o":896},{"o":897},{"o":898},{"o":899},{"o":900},{"o":901},{"o":902},{"o":903},{"o":904},{"o":905},{"o":906},{"o":907},{"o":908},{"o":909},{"o":910},{"o":911},{"o":912},{"o":913},{"o":914},{"o":915},{"o":916},{"o":917},{"o":918},{"o":919},{"o":920},{"o":921},{"o":922},{"o":923},{"o":924},{"o":925},{"o":926},{"o":927},{"o":928},{"o":929},{"o":930},{"o":931},{"o":932},{"o":933},{"o":934},{"o":935},{"o":936},{"o":937},{"o":938},{"o":939},{"o":940},{"o":941},{"o":942},{"o":943},{"o":944},{"o":945},{"o":946},{"o":947},{"o":948},{"o":949},{"o":950},{"o":951},{"o":952},{"o":953},{"o":954},{"o":955},{"o":956},{"o":957},{"o":958},{"o":959},{"o":960},{"o":961},{"o":962},{"o":963},{"o":964},{"o":965},{"o":966},{"o":967},{"o":968},{"o":969},{"o":970},{"o":971},{"o":972},{"o":973},{"o":974},{"o":975},{"o":976},{"o":977},{"o":978},{"o":979},{"o":980},{"o":981},{"o":982},{"o":983},{"o":984},{"o":985},{"o":986},{"o":987},{"o":988},{"o":989},{"o":990},{"o":991},{"o":992},{"o":993},{"o":994},{"o":995},{"o":996},{"o":997},{"o":998},{"o":999},{"o":1000}]} ], "state": { "chunk_bodies": [ - { - "account_id": "acct_interop", - "body_hex": "7b226f223a307d0a7b226f223a317d0a7b226f223a327d0a7b226f223a337d0a7b226f223a347d0a7b226f223a357d0a7b226f223a367d0a7b226f223a377d0a7b226f223a387d0a7b226f223a397d0a7b226f223a31307d0a7b226f223a31317d0a7b226f223a31327d0a7b226f223a31337d0a7b226f223a31347d0a7b226f223a31357d0a7b226f223a31367d0a7b226f223a31377d0a7b226f223a31387d0a7b226f223a31397d0a7b226f223a32307d0a7b226f223a32317d0a7b226f223a32327d0a7b226f223a32337d0a7b226f223a32347d0a7b226f223a32357d0a7b226f223a32367d0a7b226f223a32377d0a7b226f223a32387d0a7b226f223a32397d0a7b226f223a33307d0a7b226f223a33317d0a7b226f223a33327d0a7b226f223a33337d0a7b226f223a33347d0a7b226f223a33357d0a7b226f223a33367d0a7b226f223a33377d0a7b226f223a33387d0a7b226f223a33397d0a7b226f223a34307d0a7b226f223a34317d0a7b226f223a34327d0a7b226f223a34337d0a7b226f223a34347d0a7b226f223a34357d0a7b226f223a34367d0a7b226f223a34377d0a7b226f223a34387d0a7b226f223a34397d0a7b226f223a35307d0a7b226f223a35317d0a7b226f223a35327d0a7b226f223a35337d0a7b226f223a35347d0a7b226f223a35357d0a7b226f223a35367d0a7b226f223a35377d0a7b226f223a35387d0a7b226f223a35397d0a7b226f223a36307d0a7b226f223a36317d0a7b226f223a36327d0a7b226f223a36337d0a7b226f223a36347d0a7b226f223a36357d0a7b226f223a36367d0a7b226f223a36377d0a7b226f223a36387d0a7b226f223a36397d0a7b226f223a37307d0a7b226f223a37317d0a7b226f223a37327d0a7b226f223a37337d0a7b226f223a37347d0a7b226f223a37357d0a7b226f223a37367d0a7b226f223a37377d0a7b226f223a37387d0a7b226f223a37397d0a7b226f223a38307d0a7b226f223a38317d0a7b226f223a38327d0a7b226f223a38337d0a7b226f223a38347d0a7b226f223a38357d0a7b226f223a38367d0a7b226f223a38377d0a7b226f223a38387d0a7b226f223a38397d0a7b226f223a39307d0a7b226f223a39317d0a7b226f223a39327d0a7b226f223a39337d0a7b226f223a39347d0a7b226f223a39357d0a7b226f223a39367d0a7b226f223a39377d0a7b226f223a39387d0a7b226f223a39397d0a7b226f223a3130307d0a7b226f223a3130317d0a7b226f223a3130327d0a7b226f223a3130337d0a7b226f223a3130347d0a7b226f223a3130357d0a7b226f223a3130367d0a7b226f223a3130377d0a7b226f223a3130387d0a7b226f223a3130397d0a7b226f223a3131307d0a7b226f223a3131317d0a7b226f223a3131327d0a7b226f223a3131337d0a7b226f223a3131347d0a7b226f223a3131357d0a7b226f223a3131367d0a7b226f223a3131377d0a7b226f223a3131387d0a7b226f223a3131397d0a7b226f223a3132307d0a7b226f223a3132317d0a7b226f223a3132327d0a7b226f223a3132337d0a7b226f223a3132347d0a7b226f223a3132357d0a7b226f223a3132367d0a7b226f223a3132377d0a7b226f223a3132387d0a7b226f223a3132397d0a7b226f223a3133307d0a7b226f223a3133317d0a7b226f223a3133327d0a7b226f223a3133337d0a7b226f223a3133347d0a7b226f223a3133357d0a7b226f223a3133367d0a7b226f223a3133377d0a7b226f223a3133387d0a7b226f223a3133397d0a7b226f223a3134307d0a7b226f223a3134317d0a7b226f223a3134327d0a7b226f223a3134337d0a7b226f223a3134347d0a7b226f223a3134357d0a7b226f223a3134367d0a7b226f223a3134377d0a7b226f223a3134387d0a7b226f223a3134397d0a7b226f223a3135307d0a7b226f223a3135317d0a7b226f223a3135327d0a7b226f223a3135337d0a7b226f223a3135347d0a7b226f223a3135357d0a7b226f223a3135367d0a7b226f223a3135377d0a7b226f223a3135387d0a7b226f223a3135397d0a7b226f223a3136307d0a7b226f223a3136317d0a7b226f223a3136327d0a7b226f223a3136337d0a7b226f223a3136347d0a7b226f223a3136357d0a7b226f223a3136367d0a7b226f223a3136377d0a7b226f223a3136387d0a7b226f223a3136397d0a7b226f223a3137307d0a7b226f223a3137317d0a7b226f223a3137327d0a7b226f223a3137337d0a7b226f223a3137347d0a7b226f223a3137357d0a7b226f223a3137367d0a7b226f223a3137377d0a7b226f223a3137387d0a7b226f223a3137397d0a7b226f223a3138307d0a7b226f223a3138317d0a7b226f223a3138327d0a7b226f223a3138337d0a7b226f223a3138347d0a7b226f223a3138357d0a7b226f223a3138367d0a7b226f223a3138377d0a7b226f223a3138387d0a7b226f223a3138397d0a7b226f223a3139307d0a7b226f223a3139317d0a7b226f223a3139327d0a7b226f223a3139337d0a7b226f223a3139347d0a7b226f223a3139357d0a7b226f223a3139367d0a7b226f223a3139377d0a7b226f223a3139387d0a7b226f223a3139397d0a7b226f223a3230307d0a7b226f223a3230317d0a7b226f223a3230327d0a7b226f223a3230337d0a7b226f223a3230347d0a7b226f223a3230357d0a7b226f223a3230367d0a7b226f223a3230377d0a7b226f223a3230387d0a7b226f223a3230397d0a7b226f223a3231307d0a7b226f223a3231317d0a7b226f223a3231327d0a7b226f223a3231337d0a7b226f223a3231347d0a7b226f223a3231357d0a7b226f223a3231367d0a7b226f223a3231377d0a7b226f223a3231387d0a7b226f223a3231397d0a7b226f223a3232307d0a7b226f223a3232317d0a7b226f223a3232327d0a7b226f223a3232337d0a7b226f223a3232347d0a7b226f223a3232357d0a7b226f223a3232367d0a7b226f223a3232377d0a7b226f223a3232387d0a7b226f223a3232397d0a7b226f223a3233307d0a7b226f223a3233317d0a7b226f223a3233327d0a7b226f223a3233337d0a7b226f223a3233347d0a7b226f223a3233357d0a7b226f223a3233367d0a7b226f223a3233377d0a7b226f223a3233387d0a7b226f223a3233397d0a7b226f223a3234307d0a7b226f223a3234317d0a7b226f223a3234327d0a7b226f223a3234337d0a7b226f223a3234347d0a7b226f223a3234357d0a7b226f223a3234367d0a7b226f223a3234377d0a7b226f223a3234387d0a7b226f223a3234397d0a7b226f223a3235307d0a7b226f223a3235317d0a7b226f223a3235327d0a7b226f223a3235337d0a7b226f223a3235347d0a7b226f223a3235357d0a7b226f223a3235367d0a7b226f223a3235377d0a7b226f223a3235387d0a7b226f223a3235397d0a7b226f223a3236307d0a7b226f223a3236317d0a7b226f223a3236327d0a7b226f223a3236337d0a7b226f223a3236347d0a7b226f223a3236357d0a7b226f223a3236367d0a7b226f223a3236377d0a7b226f223a3236387d0a7b226f223a3236397d0a7b226f223a3237307d0a7b226f223a3237317d0a7b226f223a3237327d0a7b226f223a3237337d0a7b226f223a3237347d0a7b226f223a3237357d0a7b226f223a3237367d0a7b226f223a3237377d0a7b226f223a3237387d0a7b226f223a3237397d0a7b226f223a3238307d0a7b226f223a3238317d0a7b226f223a3238327d0a7b226f223a3238337d0a7b226f223a3238347d0a7b226f223a3238357d0a7b226f223a3238367d0a7b226f223a3238377d0a7b226f223a3238387d0a7b226f223a3238397d0a7b226f223a3239307d0a7b226f223a3239317d0a7b226f223a3239327d0a7b226f223a3239337d0a7b226f223a3239347d0a7b226f223a3239357d0a7b226f223a3239367d0a7b226f223a3239377d0a7b226f223a3239387d0a7b226f223a3239397d0a7b226f223a3330307d0a7b226f223a3330317d0a7b226f223a3330327d0a7b226f223a3330337d0a7b226f223a3330347d0a7b226f223a3330357d0a7b226f223a3330367d0a7b226f223a3330377d0a7b226f223a3330387d0a7b226f223a3330397d0a7b226f223a3331307d0a7b226f223a3331317d0a7b226f223a3331327d0a7b226f223a3331337d0a7b226f223a3331347d0a7b226f223a3331357d0a7b226f223a3331367d0a7b226f223a3331377d0a7b226f223a3331387d0a7b226f223a3331397d0a7b226f223a3332307d0a7b226f223a3332317d0a7b226f223a3332327d0a7b226f223a3332337d0a7b226f223a3332347d0a7b226f223a3332357d0a7b226f223a3332367d0a7b226f223a3332377d0a7b226f223a3332387d0a7b226f223a3332397d0a7b226f223a3333307d0a7b226f223a3333317d0a7b226f223a3333327d0a7b226f223a3333337d0a7b226f223a3333347d0a7b226f223a3333357d0a7b226f223a3333367d0a7b226f223a3333377d0a7b226f223a3333387d0a7b226f223a3333397d0a7b226f223a3334307d0a7b226f223a3334317d0a7b226f223a3334327d0a7b226f223a3334337d0a7b226f223a3334347d0a7b226f223a3334357d0a7b226f223a3334367d0a7b226f223a3334377d0a7b226f223a3334387d0a7b226f223a3334397d0a7b226f223a3335307d0a7b226f223a3335317d0a7b226f223a3335327d0a7b226f223a3335337d0a7b226f223a3335347d0a7b226f223a3335357d0a7b226f223a3335367d0a7b226f223a3335377d0a7b226f223a3335387d0a7b226f223a3335397d0a7b226f223a3336307d0a7b226f223a3336317d0a7b226f223a3336327d0a7b226f223a3336337d0a7b226f223a3336347d0a7b226f223a3336357d0a7b226f223a3336367d0a7b226f223a3336377d0a7b226f223a3336387d0a7b226f223a3336397d0a7b226f223a3337307d0a7b226f223a3337317d0a7b226f223a3337327d0a7b226f223a3337337d0a7b226f223a3337347d0a7b226f223a3337357d0a7b226f223a3337367d0a7b226f223a3337377d0a7b226f223a3337387d0a7b226f223a3337397d0a7b226f223a3338307d0a7b226f223a3338317d0a7b226f223a3338327d0a7b226f223a3338337d0a7b226f223a3338347d0a7b226f223a3338357d0a7b226f223a3338367d0a7b226f223a3338377d0a7b226f223a3338387d0a7b226f223a3338397d0a7b226f223a3339307d0a7b226f223a3339317d0a7b226f223a3339327d0a7b226f223a3339337d0a7b226f223a3339347d0a7b226f223a3339357d0a7b226f223a3339367d0a7b226f223a3339377d0a7b226f223a3339387d0a7b226f223a3339397d0a7b226f223a3430307d0a7b226f223a3430317d0a7b226f223a3430327d0a7b226f223a3430337d0a7b226f223a3430347d0a7b226f223a3430357d0a7b226f223a3430367d0a7b226f223a3430377d0a7b226f223a3430387d0a7b226f223a3430397d0a7b226f223a3431307d0a7b226f223a3431317d0a7b226f223a3431327d0a7b226f223a3431337d0a7b226f223a3431347d0a7b226f223a3431357d0a7b226f223a3431367d0a7b226f223a3431377d0a7b226f223a3431387d0a7b226f223a3431397d0a7b226f223a3432307d0a7b226f223a3432317d0a7b226f223a3432327d0a7b226f223a3432337d0a7b226f223a3432347d0a7b226f223a3432357d0a7b226f223a3432367d0a7b226f223a3432377d0a7b226f223a3432387d0a7b226f223a3432397d0a7b226f223a3433307d0a7b226f223a3433317d0a7b226f223a3433327d0a7b226f223a3433337d0a7b226f223a3433347d0a7b226f223a3433357d0a7b226f223a3433367d0a7b226f223a3433377d0a7b226f223a3433387d0a7b226f223a3433397d0a7b226f223a3434307d0a7b226f223a3434317d0a7b226f223a3434327d0a7b226f223a3434337d0a7b226f223a3434347d0a7b226f223a3434357d0a7b226f223a3434367d0a7b226f223a3434377d0a7b226f223a3434387d0a7b226f223a3434397d0a7b226f223a3435307d0a7b226f223a3435317d0a7b226f223a3435327d0a7b226f223a3435337d0a7b226f223a3435347d0a7b226f223a3435357d0a7b226f223a3435367d0a7b226f223a3435377d0a7b226f223a3435387d0a7b226f223a3435397d0a7b226f223a3436307d0a7b226f223a3436317d0a7b226f223a3436327d0a7b226f223a3436337d0a7b226f223a3436347d0a7b226f223a3436357d0a7b226f223a3436367d0a7b226f223a3436377d0a7b226f223a3436387d0a7b226f223a3436397d0a7b226f223a3437307d0a7b226f223a3437317d0a7b226f223a3437327d0a7b226f223a3437337d0a7b226f223a3437347d0a7b226f223a3437357d0a7b226f223a3437367d0a7b226f223a3437377d0a7b226f223a3437387d0a7b226f223a3437397d0a7b226f223a3438307d0a7b226f223a3438317d0a7b226f223a3438327d0a7b226f223a3438337d0a7b226f223a3438347d0a7b226f223a3438357d0a7b226f223a3438367d0a7b226f223a3438377d0a7b226f223a3438387d0a7b226f223a3438397d0a7b226f223a3439307d0a7b226f223a3439317d0a7b226f223a3439327d0a7b226f223a3439337d0a7b226f223a3439347d0a7b226f223a3439357d0a7b226f223a3439367d0a7b226f223a3439377d0a7b226f223a3439387d0a7b226f223a3439397d0a7b226f223a3530307d0a7b226f223a3530317d0a7b226f223a3530327d0a7b226f223a3530337d0a7b226f223a3530347d0a7b226f223a3530357d0a7b226f223a3530367d0a7b226f223a3530377d0a7b226f223a3530387d0a7b226f223a3530397d0a7b226f223a3531307d0a7b226f223a3531317d0a7b226f223a3531327d0a7b226f223a3531337d0a7b226f223a3531347d0a7b226f223a3531357d0a7b226f223a3531367d0a7b226f223a3531377d0a7b226f223a3531387d0a7b226f223a3531397d0a7b226f223a3532307d0a7b226f223a3532317d0a7b226f223a3532327d0a7b226f223a3532337d0a7b226f223a3532347d0a7b226f223a3532357d0a7b226f223a3532367d0a7b226f223a3532377d0a7b226f223a3532387d0a7b226f223a3532397d0a7b226f223a3533307d0a7b226f223a3533317d0a7b226f223a3533327d0a7b226f223a3533337d0a7b226f223a3533347d0a7b226f223a3533357d0a7b226f223a3533367d0a7b226f223a3533377d0a7b226f223a3533387d0a7b226f223a3533397d0a7b226f223a3534307d0a7b226f223a3534317d0a7b226f223a3534327d0a7b226f223a3534337d0a7b226f223a3534347d0a7b226f223a3534357d0a7b226f223a3534367d0a7b226f223a3534377d0a7b226f223a3534387d0a7b226f223a3534397d0a7b226f223a3535307d0a7b226f223a3535317d0a7b226f223a3535327d0a7b226f223a3535337d0a7b226f223a3535347d0a7b226f223a3535357d0a7b226f223a3535367d0a7b226f223a3535377d0a7b226f223a3535387d0a7b226f223a3535397d0a7b226f223a3536307d0a7b226f223a3536317d0a7b226f223a3536327d0a7b226f223a3536337d0a7b226f223a3536347d0a7b226f223a3536357d0a7b226f223a3536367d0a7b226f223a3536377d0a7b226f223a3536387d0a7b226f223a3536397d0a7b226f223a3537307d0a7b226f223a3537317d0a7b226f223a3537327d0a7b226f223a3537337d0a7b226f223a3537347d0a7b226f223a3537357d0a7b226f223a3537367d0a7b226f223a3537377d0a7b226f223a3537387d0a7b226f223a3537397d0a7b226f223a3538307d0a7b226f223a3538317d0a7b226f223a3538327d0a7b226f223a3538337d0a7b226f223a3538347d0a7b226f223a3538357d0a7b226f223a3538367d0a7b226f223a3538377d0a7b226f223a3538387d0a7b226f223a3538397d0a7b226f223a3539307d0a7b226f223a3539317d0a7b226f223a3539327d0a7b226f223a3539337d0a7b226f223a3539347d0a7b226f223a3539357d0a7b226f223a3539367d0a7b226f223a3539377d0a7b226f223a3539387d0a7b226f223a3539397d0a7b226f223a3630307d0a7b226f223a3630317d0a7b226f223a3630327d0a7b226f223a3630337d0a7b226f223a3630347d0a7b226f223a3630357d0a7b226f223a3630367d0a7b226f223a3630377d0a7b226f223a3630387d0a7b226f223a3630397d0a7b226f223a3631307d0a7b226f223a3631317d0a7b226f223a3631327d0a7b226f223a3631337d0a7b226f223a3631347d0a7b226f223a3631357d0a7b226f223a3631367d0a7b226f223a3631377d0a7b226f223a3631387d0a7b226f223a3631397d0a7b226f223a3632307d0a7b226f223a3632317d0a7b226f223a3632327d0a7b226f223a3632337d0a7b226f223a3632347d0a7b226f223a3632357d0a7b226f223a3632367d0a7b226f223a3632377d0a7b226f223a3632387d0a7b226f223a3632397d0a7b226f223a3633307d0a7b226f223a3633317d0a7b226f223a3633327d0a7b226f223a3633337d0a7b226f223a3633347d0a7b226f223a3633357d0a7b226f223a3633367d0a7b226f223a3633377d0a7b226f223a3633387d0a7b226f223a3633397d0a7b226f223a3634307d0a7b226f223a3634317d0a7b226f223a3634327d0a7b226f223a3634337d0a7b226f223a3634347d0a7b226f223a3634357d0a7b226f223a3634367d0a7b226f223a3634377d0a7b226f223a3634387d0a7b226f223a3634397d0a7b226f223a3635307d0a7b226f223a3635317d0a7b226f223a3635327d0a7b226f223a3635337d0a7b226f223a3635347d0a7b226f223a3635357d0a7b226f223a3635367d0a7b226f223a3635377d0a7b226f223a3635387d0a7b226f223a3635397d0a7b226f223a3636307d0a7b226f223a3636317d0a7b226f223a3636327d0a7b226f223a3636337d0a7b226f223a3636347d0a7b226f223a3636357d0a7b226f223a3636367d0a7b226f223a3636377d0a7b226f223a3636387d0a7b226f223a3636397d0a7b226f223a3637307d0a7b226f223a3637317d0a7b226f223a3637327d0a7b226f223a3637337d0a7b226f223a3637347d0a7b226f223a3637357d0a7b226f223a3637367d0a7b226f223a3637377d0a7b226f223a3637387d0a7b226f223a3637397d0a7b226f223a3638307d0a7b226f223a3638317d0a7b226f223a3638327d0a7b226f223a3638337d0a7b226f223a3638347d0a7b226f223a3638357d0a7b226f223a3638367d0a7b226f223a3638377d0a7b226f223a3638387d0a7b226f223a3638397d0a7b226f223a3639307d0a7b226f223a3639317d0a7b226f223a3639327d0a7b226f223a3639337d0a7b226f223a3639347d0a7b226f223a3639357d0a7b226f223a3639367d0a7b226f223a3639377d0a7b226f223a3639387d0a7b226f223a3639397d0a7b226f223a3730307d0a7b226f223a3730317d0a7b226f223a3730327d0a7b226f223a3730337d0a7b226f223a3730347d0a7b226f223a3730357d0a7b226f223a3730367d0a7b226f223a3730377d0a7b226f223a3730387d0a7b226f223a3730397d0a7b226f223a3731307d0a7b226f223a3731317d0a7b226f223a3731327d0a7b226f223a3731337d0a7b226f223a3731347d0a7b226f223a3731357d0a7b226f223a3731367d0a7b226f223a3731377d0a7b226f223a3731387d0a7b226f223a3731397d0a7b226f223a3732307d0a7b226f223a3732317d0a7b226f223a3732327d0a7b226f223a3732337d0a7b226f223a3732347d0a7b226f223a3732357d0a7b226f223a3732367d0a7b226f223a3732377d0a7b226f223a3732387d0a7b226f223a3732397d0a7b226f223a3733307d0a7b226f223a3733317d0a7b226f223a3733327d0a7b226f223a3733337d0a7b226f223a3733347d0a7b226f223a3733357d0a7b226f223a3733367d0a7b226f223a3733377d0a7b226f223a3733387d0a7b226f223a3733397d0a7b226f223a3734307d0a7b226f223a3734317d0a7b226f223a3734327d0a7b226f223a3734337d0a7b226f223a3734347d0a7b226f223a3734357d0a7b226f223a3734367d0a7b226f223a3734377d0a7b226f223a3734387d0a7b226f223a3734397d0a7b226f223a3735307d0a7b226f223a3735317d0a7b226f223a3735327d0a7b226f223a3735337d0a7b226f223a3735347d0a7b226f223a3735357d0a7b226f223a3735367d0a7b226f223a3735377d0a7b226f223a3735387d0a7b226f223a3735397d0a7b226f223a3736307d0a7b226f223a3736317d0a7b226f223a3736327d0a7b226f223a3736337d0a7b226f223a3736347d0a7b226f223a3736357d0a7b226f223a3736367d0a7b226f223a3736377d0a7b226f223a3736387d0a7b226f223a3736397d0a7b226f223a3737307d0a7b226f223a3737317d0a7b226f223a3737327d0a7b226f223a3737337d0a7b226f223a3737347d0a7b226f223a3737357d0a7b226f223a3737367d0a7b226f223a3737377d0a7b226f223a3737387d0a7b226f223a3737397d0a7b226f223a3738307d0a7b226f223a3738317d0a7b226f223a3738327d0a7b226f223a3738337d0a7b226f223a3738347d0a7b226f223a3738357d0a7b226f223a3738367d0a7b226f223a3738377d0a7b226f223a3738387d0a7b226f223a3738397d0a7b226f223a3739307d0a7b226f223a3739317d0a7b226f223a3739327d0a7b226f223a3739337d0a7b226f223a3739347d0a7b226f223a3739357d0a7b226f223a3739367d0a7b226f223a3739377d0a7b226f223a3739387d0a7b226f223a3739397d0a7b226f223a3830307d0a7b226f223a3830317d0a7b226f223a3830327d0a7b226f223a3830337d0a7b226f223a3830347d0a7b226f223a3830357d0a7b226f223a3830367d0a7b226f223a3830377d0a7b226f223a3830387d0a7b226f223a3830397d0a7b226f223a3831307d0a7b226f223a3831317d0a7b226f223a3831327d0a7b226f223a3831337d0a7b226f223a3831347d0a7b226f223a3831357d0a7b226f223a3831367d0a7b226f223a3831377d0a7b226f223a3831387d0a7b226f223a3831397d0a7b226f223a3832307d0a7b226f223a3832317d0a7b226f223a3832327d0a7b226f223a3832337d0a7b226f223a3832347d0a7b226f223a3832357d0a7b226f223a3832367d0a7b226f223a3832377d0a7b226f223a3832387d0a7b226f223a3832397d0a7b226f223a3833307d0a7b226f223a3833317d0a7b226f223a3833327d0a7b226f223a3833337d0a7b226f223a3833347d0a7b226f223a3833357d0a7b226f223a3833367d0a7b226f223a3833377d0a7b226f223a3833387d0a7b226f223a3833397d0a7b226f223a3834307d0a7b226f223a3834317d0a7b226f223a3834327d0a7b226f223a3834337d0a7b226f223a3834347d0a7b226f223a3834357d0a7b226f223a3834367d0a7b226f223a3834377d0a7b226f223a3834387d0a7b226f223a3834397d0a7b226f223a3835307d0a7b226f223a3835317d0a7b226f223a3835327d0a7b226f223a3835337d0a7b226f223a3835347d0a7b226f223a3835357d0a7b226f223a3835367d0a7b226f223a3835377d0a7b226f223a3835387d0a7b226f223a3835397d0a7b226f223a3836307d0a7b226f223a3836317d0a7b226f223a3836327d0a7b226f223a3836337d0a7b226f223a3836347d0a7b226f223a3836357d0a7b226f223a3836367d0a7b226f223a3836377d0a7b226f223a3836387d0a7b226f223a3836397d0a7b226f223a3837307d0a7b226f223a3837317d0a7b226f223a3837327d0a7b226f223a3837337d0a7b226f223a3837347d0a7b226f223a3837357d0a7b226f223a3837367d0a7b226f223a3837377d0a7b226f223a3837387d0a7b226f223a3837397d0a7b226f223a3838307d0a7b226f223a3838317d0a7b226f223a3838327d0a7b226f223a3838337d0a7b226f223a3838347d0a7b226f223a3838357d0a7b226f223a3838367d0a7b226f223a3838377d0a7b226f223a3838387d0a7b226f223a3838397d0a7b226f223a3839307d0a7b226f223a3839317d0a7b226f223a3839327d0a7b226f223a3839337d0a7b226f223a3839347d0a7b226f223a3839357d0a7b226f223a3839367d0a7b226f223a3839377d0a7b226f223a3839387d0a7b226f223a3839397d0a7b226f223a3930307d0a7b226f223a3930317d0a7b226f223a3930327d0a7b226f223a3930337d0a7b226f223a3930347d0a7b226f223a3930357d0a7b226f223a3930367d0a7b226f223a3930377d0a7b226f223a3930387d0a7b226f223a3930397d0a7b226f223a3931307d0a7b226f223a3931317d0a7b226f223a3931327d0a7b226f223a3931337d0a7b226f223a3931347d0a7b226f223a3931357d0a7b226f223a3931367d0a7b226f223a3931377d0a7b226f223a3931387d0a7b226f223a3931397d0a7b226f223a3932307d0a7b226f223a3932317d0a7b226f223a3932327d0a7b226f223a3932337d0a7b226f223a3932347d0a7b226f223a3932357d0a7b226f223a3932367d0a7b226f223a3932377d0a7b226f223a3932387d0a7b226f223a3932397d0a7b226f223a3933307d0a7b226f223a3933317d0a7b226f223a3933327d0a7b226f223a3933337d0a7b226f223a3933347d0a7b226f223a3933357d0a7b226f223a3933367d0a7b226f223a3933377d0a7b226f223a3933387d0a7b226f223a3933397d0a7b226f223a3934307d0a7b226f223a3934317d0a7b226f223a3934327d0a7b226f223a3934337d0a7b226f223a3934347d0a7b226f223a3934357d0a7b226f223a3934367d0a7b226f223a3934377d0a7b226f223a3934387d0a7b226f223a3934397d0a7b226f223a3935307d0a7b226f223a3935317d0a7b226f223a3935327d0a7b226f223a3935337d0a7b226f223a3935347d0a7b226f223a3935357d0a7b226f223a3935367d0a7b226f223a3935377d0a7b226f223a3935387d0a7b226f223a3935397d0a7b226f223a3936307d0a7b226f223a3936317d0a7b226f223a3936327d0a7b226f223a3936337d0a7b226f223a3936347d0a7b226f223a3936357d0a7b226f223a3936367d0a7b226f223a3936377d0a7b226f223a3936387d0a7b226f223a3936397d0a7b226f223a3937307d0a7b226f223a3937317d0a7b226f223a3937327d0a7b226f223a3937337d0a7b226f223a3937347d0a7b226f223a3937357d0a7b226f223a3937367d0a7b226f223a3937377d0a7b226f223a3937387d0a7b226f223a3937397d0a7b226f223a3938307d0a7b226f223a3938317d0a7b226f223a3938327d0a7b226f223a3938337d0a7b226f223a3938347d0a7b226f223a3938357d0a7b226f223a3938367d0a7b226f223a3938377d0a7b226f223a3938387d0a7b226f223a3938397d0a7b226f223a3939307d0a7b226f223a3939317d0a7b226f223a3939327d0a7b226f223a3939337d0a7b226f223a3939347d0a7b226f223a3939357d0a7b226f223a3939367d0a7b226f223a3939377d0a7b226f223a3939387d0a7b226f223a3939397d0a7b226f223a313030307d0a", - "obligation_id": "robl_interop", - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277" - }, - { - "account_id": "acct_interop", - "body_hex": "7b2261223a322c22636f6e74726f6c223a225c75303030375c6e5c225c5c222c226e616d65223a22636166c3a920f09f988020e697a5e69cace8aa9e222c22736570223a22e280a8222c227a223a317d0a7b22666c6f6174223a302e33303030303030303030303030303030342c226d61785f696e74223a393030373139393235343734303939312c226e223a302c226e6567223a2d31322e352c226e6573746564223a7b2261223a5b5d2c227a223a5b312c7b2261223a747275652c2262223a6e756c6c7d5d7d2c2274696e79223a31652d377d0a", - "obligation_id": "robl_interop", - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - }, - { - "account_id": "acct_interop", - "body_hex": "7b2264617465223a22323032362d31302d3031222c22696d7072657373696f6e73223a313230302c226d656469615f6275795f6964223a226d625f31222c227061636b6167655f6964223a22706b675f31222c227370656e64223a2234322e3137227d0a", - "obligation_id": "robl_interop", - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } + {"account_id":"acct_interop","body_hex":"7b226f223a307d0a7b226f223a317d0a7b226f223a327d0a7b226f223a337d0a7b226f223a347d0a7b226f223a357d0a7b226f223a367d0a7b226f223a377d0a7b226f223a387d0a7b226f223a397d0a7b226f223a31307d0a7b226f223a31317d0a7b226f223a31327d0a7b226f223a31337d0a7b226f223a31347d0a7b226f223a31357d0a7b226f223a31367d0a7b226f223a31377d0a7b226f223a31387d0a7b226f223a31397d0a7b226f223a32307d0a7b226f223a32317d0a7b226f223a32327d0a7b226f223a32337d0a7b226f223a32347d0a7b226f223a32357d0a7b226f223a32367d0a7b226f223a32377d0a7b226f223a32387d0a7b226f223a32397d0a7b226f223a33307d0a7b226f223a33317d0a7b226f223a33327d0a7b226f223a33337d0a7b226f223a33347d0a7b226f223a33357d0a7b226f223a33367d0a7b226f223a33377d0a7b226f223a33387d0a7b226f223a33397d0a7b226f223a34307d0a7b226f223a34317d0a7b226f223a34327d0a7b226f223a34337d0a7b226f223a34347d0a7b226f223a34357d0a7b226f223a34367d0a7b226f223a34377d0a7b226f223a34387d0a7b226f223a34397d0a7b226f223a35307d0a7b226f223a35317d0a7b226f223a35327d0a7b226f223a35337d0a7b226f223a35347d0a7b226f223a35357d0a7b226f223a35367d0a7b226f223a35377d0a7b226f223a35387d0a7b226f223a35397d0a7b226f223a36307d0a7b226f223a36317d0a7b226f223a36327d0a7b226f223a36337d0a7b226f223a36347d0a7b226f223a36357d0a7b226f223a36367d0a7b226f223a36377d0a7b226f223a36387d0a7b226f223a36397d0a7b226f223a37307d0a7b226f223a37317d0a7b226f223a37327d0a7b226f223a37337d0a7b226f223a37347d0a7b226f223a37357d0a7b226f223a37367d0a7b226f223a37377d0a7b226f223a37387d0a7b226f223a37397d0a7b226f223a38307d0a7b226f223a38317d0a7b226f223a38327d0a7b226f223a38337d0a7b226f223a38347d0a7b226f223a38357d0a7b226f223a38367d0a7b226f223a38377d0a7b226f223a38387d0a7b226f223a38397d0a7b226f223a39307d0a7b226f223a39317d0a7b226f223a39327d0a7b226f223a39337d0a7b226f223a39347d0a7b226f223a39357d0a7b226f223a39367d0a7b226f223a39377d0a7b226f223a39387d0a7b226f223a39397d0a7b226f223a3130307d0a7b226f223a3130317d0a7b226f223a3130327d0a7b226f223a3130337d0a7b226f223a3130347d0a7b226f223a3130357d0a7b226f223a3130367d0a7b226f223a3130377d0a7b226f223a3130387d0a7b226f223a3130397d0a7b226f223a3131307d0a7b226f223a3131317d0a7b226f223a3131327d0a7b226f223a3131337d0a7b226f223a3131347d0a7b226f223a3131357d0a7b226f223a3131367d0a7b226f223a3131377d0a7b226f223a3131387d0a7b226f223a3131397d0a7b226f223a3132307d0a7b226f223a3132317d0a7b226f223a3132327d0a7b226f223a3132337d0a7b226f223a3132347d0a7b226f223a3132357d0a7b226f223a3132367d0a7b226f223a3132377d0a7b226f223a3132387d0a7b226f223a3132397d0a7b226f223a3133307d0a7b226f223a3133317d0a7b226f223a3133327d0a7b226f223a3133337d0a7b226f223a3133347d0a7b226f223a3133357d0a7b226f223a3133367d0a7b226f223a3133377d0a7b226f223a3133387d0a7b226f223a3133397d0a7b226f223a3134307d0a7b226f223a3134317d0a7b226f223a3134327d0a7b226f223a3134337d0a7b226f223a3134347d0a7b226f223a3134357d0a7b226f223a3134367d0a7b226f223a3134377d0a7b226f223a3134387d0a7b226f223a3134397d0a7b226f223a3135307d0a7b226f223a3135317d0a7b226f223a3135327d0a7b226f223a3135337d0a7b226f223a3135347d0a7b226f223a3135357d0a7b226f223a3135367d0a7b226f223a3135377d0a7b226f223a3135387d0a7b226f223a3135397d0a7b226f223a3136307d0a7b226f223a3136317d0a7b226f223a3136327d0a7b226f223a3136337d0a7b226f223a3136347d0a7b226f223a3136357d0a7b226f223a3136367d0a7b226f223a3136377d0a7b226f223a3136387d0a7b226f223a3136397d0a7b226f223a3137307d0a7b226f223a3137317d0a7b226f223a3137327d0a7b226f223a3137337d0a7b226f223a3137347d0a7b226f223a3137357d0a7b226f223a3137367d0a7b226f223a3137377d0a7b226f223a3137387d0a7b226f223a3137397d0a7b226f223a3138307d0a7b226f223a3138317d0a7b226f223a3138327d0a7b226f223a3138337d0a7b226f223a3138347d0a7b226f223a3138357d0a7b226f223a3138367d0a7b226f223a3138377d0a7b226f223a3138387d0a7b226f223a3138397d0a7b226f223a3139307d0a7b226f223a3139317d0a7b226f223a3139327d0a7b226f223a3139337d0a7b226f223a3139347d0a7b226f223a3139357d0a7b226f223a3139367d0a7b226f223a3139377d0a7b226f223a3139387d0a7b226f223a3139397d0a7b226f223a3230307d0a7b226f223a3230317d0a7b226f223a3230327d0a7b226f223a3230337d0a7b226f223a3230347d0a7b226f223a3230357d0a7b226f223a3230367d0a7b226f223a3230377d0a7b226f223a3230387d0a7b226f223a3230397d0a7b226f223a3231307d0a7b226f223a3231317d0a7b226f223a3231327d0a7b226f223a3231337d0a7b226f223a3231347d0a7b226f223a3231357d0a7b226f223a3231367d0a7b226f223a3231377d0a7b226f223a3231387d0a7b226f223a3231397d0a7b226f223a3232307d0a7b226f223a3232317d0a7b226f223a3232327d0a7b226f223a3232337d0a7b226f223a3232347d0a7b226f223a3232357d0a7b226f223a3232367d0a7b226f223a3232377d0a7b226f223a3232387d0a7b226f223a3232397d0a7b226f223a3233307d0a7b226f223a3233317d0a7b226f223a3233327d0a7b226f223a3233337d0a7b226f223a3233347d0a7b226f223a3233357d0a7b226f223a3233367d0a7b226f223a3233377d0a7b226f223a3233387d0a7b226f223a3233397d0a7b226f223a3234307d0a7b226f223a3234317d0a7b226f223a3234327d0a7b226f223a3234337d0a7b226f223a3234347d0a7b226f223a3234357d0a7b226f223a3234367d0a7b226f223a3234377d0a7b226f223a3234387d0a7b226f223a3234397d0a7b226f223a3235307d0a7b226f223a3235317d0a7b226f223a3235327d0a7b226f223a3235337d0a7b226f223a3235347d0a7b226f223a3235357d0a7b226f223a3235367d0a7b226f223a3235377d0a7b226f223a3235387d0a7b226f223a3235397d0a7b226f223a3236307d0a7b226f223a3236317d0a7b226f223a3236327d0a7b226f223a3236337d0a7b226f223a3236347d0a7b226f223a3236357d0a7b226f223a3236367d0a7b226f223a3236377d0a7b226f223a3236387d0a7b226f223a3236397d0a7b226f223a3237307d0a7b226f223a3237317d0a7b226f223a3237327d0a7b226f223a3237337d0a7b226f223a3237347d0a7b226f223a3237357d0a7b226f223a3237367d0a7b226f223a3237377d0a7b226f223a3237387d0a7b226f223a3237397d0a7b226f223a3238307d0a7b226f223a3238317d0a7b226f223a3238327d0a7b226f223a3238337d0a7b226f223a3238347d0a7b226f223a3238357d0a7b226f223a3238367d0a7b226f223a3238377d0a7b226f223a3238387d0a7b226f223a3238397d0a7b226f223a3239307d0a7b226f223a3239317d0a7b226f223a3239327d0a7b226f223a3239337d0a7b226f223a3239347d0a7b226f223a3239357d0a7b226f223a3239367d0a7b226f223a3239377d0a7b226f223a3239387d0a7b226f223a3239397d0a7b226f223a3330307d0a7b226f223a3330317d0a7b226f223a3330327d0a7b226f223a3330337d0a7b226f223a3330347d0a7b226f223a3330357d0a7b226f223a3330367d0a7b226f223a3330377d0a7b226f223a3330387d0a7b226f223a3330397d0a7b226f223a3331307d0a7b226f223a3331317d0a7b226f223a3331327d0a7b226f223a3331337d0a7b226f223a3331347d0a7b226f223a3331357d0a7b226f223a3331367d0a7b226f223a3331377d0a7b226f223a3331387d0a7b226f223a3331397d0a7b226f223a3332307d0a7b226f223a3332317d0a7b226f223a3332327d0a7b226f223a3332337d0a7b226f223a3332347d0a7b226f223a3332357d0a7b226f223a3332367d0a7b226f223a3332377d0a7b226f223a3332387d0a7b226f223a3332397d0a7b226f223a3333307d0a7b226f223a3333317d0a7b226f223a3333327d0a7b226f223a3333337d0a7b226f223a3333347d0a7b226f223a3333357d0a7b226f223a3333367d0a7b226f223a3333377d0a7b226f223a3333387d0a7b226f223a3333397d0a7b226f223a3334307d0a7b226f223a3334317d0a7b226f223a3334327d0a7b226f223a3334337d0a7b226f223a3334347d0a7b226f223a3334357d0a7b226f223a3334367d0a7b226f223a3334377d0a7b226f223a3334387d0a7b226f223a3334397d0a7b226f223a3335307d0a7b226f223a3335317d0a7b226f223a3335327d0a7b226f223a3335337d0a7b226f223a3335347d0a7b226f223a3335357d0a7b226f223a3335367d0a7b226f223a3335377d0a7b226f223a3335387d0a7b226f223a3335397d0a7b226f223a3336307d0a7b226f223a3336317d0a7b226f223a3336327d0a7b226f223a3336337d0a7b226f223a3336347d0a7b226f223a3336357d0a7b226f223a3336367d0a7b226f223a3336377d0a7b226f223a3336387d0a7b226f223a3336397d0a7b226f223a3337307d0a7b226f223a3337317d0a7b226f223a3337327d0a7b226f223a3337337d0a7b226f223a3337347d0a7b226f223a3337357d0a7b226f223a3337367d0a7b226f223a3337377d0a7b226f223a3337387d0a7b226f223a3337397d0a7b226f223a3338307d0a7b226f223a3338317d0a7b226f223a3338327d0a7b226f223a3338337d0a7b226f223a3338347d0a7b226f223a3338357d0a7b226f223a3338367d0a7b226f223a3338377d0a7b226f223a3338387d0a7b226f223a3338397d0a7b226f223a3339307d0a7b226f223a3339317d0a7b226f223a3339327d0a7b226f223a3339337d0a7b226f223a3339347d0a7b226f223a3339357d0a7b226f223a3339367d0a7b226f223a3339377d0a7b226f223a3339387d0a7b226f223a3339397d0a7b226f223a3430307d0a7b226f223a3430317d0a7b226f223a3430327d0a7b226f223a3430337d0a7b226f223a3430347d0a7b226f223a3430357d0a7b226f223a3430367d0a7b226f223a3430377d0a7b226f223a3430387d0a7b226f223a3430397d0a7b226f223a3431307d0a7b226f223a3431317d0a7b226f223a3431327d0a7b226f223a3431337d0a7b226f223a3431347d0a7b226f223a3431357d0a7b226f223a3431367d0a7b226f223a3431377d0a7b226f223a3431387d0a7b226f223a3431397d0a7b226f223a3432307d0a7b226f223a3432317d0a7b226f223a3432327d0a7b226f223a3432337d0a7b226f223a3432347d0a7b226f223a3432357d0a7b226f223a3432367d0a7b226f223a3432377d0a7b226f223a3432387d0a7b226f223a3432397d0a7b226f223a3433307d0a7b226f223a3433317d0a7b226f223a3433327d0a7b226f223a3433337d0a7b226f223a3433347d0a7b226f223a3433357d0a7b226f223a3433367d0a7b226f223a3433377d0a7b226f223a3433387d0a7b226f223a3433397d0a7b226f223a3434307d0a7b226f223a3434317d0a7b226f223a3434327d0a7b226f223a3434337d0a7b226f223a3434347d0a7b226f223a3434357d0a7b226f223a3434367d0a7b226f223a3434377d0a7b226f223a3434387d0a7b226f223a3434397d0a7b226f223a3435307d0a7b226f223a3435317d0a7b226f223a3435327d0a7b226f223a3435337d0a7b226f223a3435347d0a7b226f223a3435357d0a7b226f223a3435367d0a7b226f223a3435377d0a7b226f223a3435387d0a7b226f223a3435397d0a7b226f223a3436307d0a7b226f223a3436317d0a7b226f223a3436327d0a7b226f223a3436337d0a7b226f223a3436347d0a7b226f223a3436357d0a7b226f223a3436367d0a7b226f223a3436377d0a7b226f223a3436387d0a7b226f223a3436397d0a7b226f223a3437307d0a7b226f223a3437317d0a7b226f223a3437327d0a7b226f223a3437337d0a7b226f223a3437347d0a7b226f223a3437357d0a7b226f223a3437367d0a7b226f223a3437377d0a7b226f223a3437387d0a7b226f223a3437397d0a7b226f223a3438307d0a7b226f223a3438317d0a7b226f223a3438327d0a7b226f223a3438337d0a7b226f223a3438347d0a7b226f223a3438357d0a7b226f223a3438367d0a7b226f223a3438377d0a7b226f223a3438387d0a7b226f223a3438397d0a7b226f223a3439307d0a7b226f223a3439317d0a7b226f223a3439327d0a7b226f223a3439337d0a7b226f223a3439347d0a7b226f223a3439357d0a7b226f223a3439367d0a7b226f223a3439377d0a7b226f223a3439387d0a7b226f223a3439397d0a7b226f223a3530307d0a7b226f223a3530317d0a7b226f223a3530327d0a7b226f223a3530337d0a7b226f223a3530347d0a7b226f223a3530357d0a7b226f223a3530367d0a7b226f223a3530377d0a7b226f223a3530387d0a7b226f223a3530397d0a7b226f223a3531307d0a7b226f223a3531317d0a7b226f223a3531327d0a7b226f223a3531337d0a7b226f223a3531347d0a7b226f223a3531357d0a7b226f223a3531367d0a7b226f223a3531377d0a7b226f223a3531387d0a7b226f223a3531397d0a7b226f223a3532307d0a7b226f223a3532317d0a7b226f223a3532327d0a7b226f223a3532337d0a7b226f223a3532347d0a7b226f223a3532357d0a7b226f223a3532367d0a7b226f223a3532377d0a7b226f223a3532387d0a7b226f223a3532397d0a7b226f223a3533307d0a7b226f223a3533317d0a7b226f223a3533327d0a7b226f223a3533337d0a7b226f223a3533347d0a7b226f223a3533357d0a7b226f223a3533367d0a7b226f223a3533377d0a7b226f223a3533387d0a7b226f223a3533397d0a7b226f223a3534307d0a7b226f223a3534317d0a7b226f223a3534327d0a7b226f223a3534337d0a7b226f223a3534347d0a7b226f223a3534357d0a7b226f223a3534367d0a7b226f223a3534377d0a7b226f223a3534387d0a7b226f223a3534397d0a7b226f223a3535307d0a7b226f223a3535317d0a7b226f223a3535327d0a7b226f223a3535337d0a7b226f223a3535347d0a7b226f223a3535357d0a7b226f223a3535367d0a7b226f223a3535377d0a7b226f223a3535387d0a7b226f223a3535397d0a7b226f223a3536307d0a7b226f223a3536317d0a7b226f223a3536327d0a7b226f223a3536337d0a7b226f223a3536347d0a7b226f223a3536357d0a7b226f223a3536367d0a7b226f223a3536377d0a7b226f223a3536387d0a7b226f223a3536397d0a7b226f223a3537307d0a7b226f223a3537317d0a7b226f223a3537327d0a7b226f223a3537337d0a7b226f223a3537347d0a7b226f223a3537357d0a7b226f223a3537367d0a7b226f223a3537377d0a7b226f223a3537387d0a7b226f223a3537397d0a7b226f223a3538307d0a7b226f223a3538317d0a7b226f223a3538327d0a7b226f223a3538337d0a7b226f223a3538347d0a7b226f223a3538357d0a7b226f223a3538367d0a7b226f223a3538377d0a7b226f223a3538387d0a7b226f223a3538397d0a7b226f223a3539307d0a7b226f223a3539317d0a7b226f223a3539327d0a7b226f223a3539337d0a7b226f223a3539347d0a7b226f223a3539357d0a7b226f223a3539367d0a7b226f223a3539377d0a7b226f223a3539387d0a7b226f223a3539397d0a7b226f223a3630307d0a7b226f223a3630317d0a7b226f223a3630327d0a7b226f223a3630337d0a7b226f223a3630347d0a7b226f223a3630357d0a7b226f223a3630367d0a7b226f223a3630377d0a7b226f223a3630387d0a7b226f223a3630397d0a7b226f223a3631307d0a7b226f223a3631317d0a7b226f223a3631327d0a7b226f223a3631337d0a7b226f223a3631347d0a7b226f223a3631357d0a7b226f223a3631367d0a7b226f223a3631377d0a7b226f223a3631387d0a7b226f223a3631397d0a7b226f223a3632307d0a7b226f223a3632317d0a7b226f223a3632327d0a7b226f223a3632337d0a7b226f223a3632347d0a7b226f223a3632357d0a7b226f223a3632367d0a7b226f223a3632377d0a7b226f223a3632387d0a7b226f223a3632397d0a7b226f223a3633307d0a7b226f223a3633317d0a7b226f223a3633327d0a7b226f223a3633337d0a7b226f223a3633347d0a7b226f223a3633357d0a7b226f223a3633367d0a7b226f223a3633377d0a7b226f223a3633387d0a7b226f223a3633397d0a7b226f223a3634307d0a7b226f223a3634317d0a7b226f223a3634327d0a7b226f223a3634337d0a7b226f223a3634347d0a7b226f223a3634357d0a7b226f223a3634367d0a7b226f223a3634377d0a7b226f223a3634387d0a7b226f223a3634397d0a7b226f223a3635307d0a7b226f223a3635317d0a7b226f223a3635327d0a7b226f223a3635337d0a7b226f223a3635347d0a7b226f223a3635357d0a7b226f223a3635367d0a7b226f223a3635377d0a7b226f223a3635387d0a7b226f223a3635397d0a7b226f223a3636307d0a7b226f223a3636317d0a7b226f223a3636327d0a7b226f223a3636337d0a7b226f223a3636347d0a7b226f223a3636357d0a7b226f223a3636367d0a7b226f223a3636377d0a7b226f223a3636387d0a7b226f223a3636397d0a7b226f223a3637307d0a7b226f223a3637317d0a7b226f223a3637327d0a7b226f223a3637337d0a7b226f223a3637347d0a7b226f223a3637357d0a7b226f223a3637367d0a7b226f223a3637377d0a7b226f223a3637387d0a7b226f223a3637397d0a7b226f223a3638307d0a7b226f223a3638317d0a7b226f223a3638327d0a7b226f223a3638337d0a7b226f223a3638347d0a7b226f223a3638357d0a7b226f223a3638367d0a7b226f223a3638377d0a7b226f223a3638387d0a7b226f223a3638397d0a7b226f223a3639307d0a7b226f223a3639317d0a7b226f223a3639327d0a7b226f223a3639337d0a7b226f223a3639347d0a7b226f223a3639357d0a7b226f223a3639367d0a7b226f223a3639377d0a7b226f223a3639387d0a7b226f223a3639397d0a7b226f223a3730307d0a7b226f223a3730317d0a7b226f223a3730327d0a7b226f223a3730337d0a7b226f223a3730347d0a7b226f223a3730357d0a7b226f223a3730367d0a7b226f223a3730377d0a7b226f223a3730387d0a7b226f223a3730397d0a7b226f223a3731307d0a7b226f223a3731317d0a7b226f223a3731327d0a7b226f223a3731337d0a7b226f223a3731347d0a7b226f223a3731357d0a7b226f223a3731367d0a7b226f223a3731377d0a7b226f223a3731387d0a7b226f223a3731397d0a7b226f223a3732307d0a7b226f223a3732317d0a7b226f223a3732327d0a7b226f223a3732337d0a7b226f223a3732347d0a7b226f223a3732357d0a7b226f223a3732367d0a7b226f223a3732377d0a7b226f223a3732387d0a7b226f223a3732397d0a7b226f223a3733307d0a7b226f223a3733317d0a7b226f223a3733327d0a7b226f223a3733337d0a7b226f223a3733347d0a7b226f223a3733357d0a7b226f223a3733367d0a7b226f223a3733377d0a7b226f223a3733387d0a7b226f223a3733397d0a7b226f223a3734307d0a7b226f223a3734317d0a7b226f223a3734327d0a7b226f223a3734337d0a7b226f223a3734347d0a7b226f223a3734357d0a7b226f223a3734367d0a7b226f223a3734377d0a7b226f223a3734387d0a7b226f223a3734397d0a7b226f223a3735307d0a7b226f223a3735317d0a7b226f223a3735327d0a7b226f223a3735337d0a7b226f223a3735347d0a7b226f223a3735357d0a7b226f223a3735367d0a7b226f223a3735377d0a7b226f223a3735387d0a7b226f223a3735397d0a7b226f223a3736307d0a7b226f223a3736317d0a7b226f223a3736327d0a7b226f223a3736337d0a7b226f223a3736347d0a7b226f223a3736357d0a7b226f223a3736367d0a7b226f223a3736377d0a7b226f223a3736387d0a7b226f223a3736397d0a7b226f223a3737307d0a7b226f223a3737317d0a7b226f223a3737327d0a7b226f223a3737337d0a7b226f223a3737347d0a7b226f223a3737357d0a7b226f223a3737367d0a7b226f223a3737377d0a7b226f223a3737387d0a7b226f223a3737397d0a7b226f223a3738307d0a7b226f223a3738317d0a7b226f223a3738327d0a7b226f223a3738337d0a7b226f223a3738347d0a7b226f223a3738357d0a7b226f223a3738367d0a7b226f223a3738377d0a7b226f223a3738387d0a7b226f223a3738397d0a7b226f223a3739307d0a7b226f223a3739317d0a7b226f223a3739327d0a7b226f223a3739337d0a7b226f223a3739347d0a7b226f223a3739357d0a7b226f223a3739367d0a7b226f223a3739377d0a7b226f223a3739387d0a7b226f223a3739397d0a7b226f223a3830307d0a7b226f223a3830317d0a7b226f223a3830327d0a7b226f223a3830337d0a7b226f223a3830347d0a7b226f223a3830357d0a7b226f223a3830367d0a7b226f223a3830377d0a7b226f223a3830387d0a7b226f223a3830397d0a7b226f223a3831307d0a7b226f223a3831317d0a7b226f223a3831327d0a7b226f223a3831337d0a7b226f223a3831347d0a7b226f223a3831357d0a7b226f223a3831367d0a7b226f223a3831377d0a7b226f223a3831387d0a7b226f223a3831397d0a7b226f223a3832307d0a7b226f223a3832317d0a7b226f223a3832327d0a7b226f223a3832337d0a7b226f223a3832347d0a7b226f223a3832357d0a7b226f223a3832367d0a7b226f223a3832377d0a7b226f223a3832387d0a7b226f223a3832397d0a7b226f223a3833307d0a7b226f223a3833317d0a7b226f223a3833327d0a7b226f223a3833337d0a7b226f223a3833347d0a7b226f223a3833357d0a7b226f223a3833367d0a7b226f223a3833377d0a7b226f223a3833387d0a7b226f223a3833397d0a7b226f223a3834307d0a7b226f223a3834317d0a7b226f223a3834327d0a7b226f223a3834337d0a7b226f223a3834347d0a7b226f223a3834357d0a7b226f223a3834367d0a7b226f223a3834377d0a7b226f223a3834387d0a7b226f223a3834397d0a7b226f223a3835307d0a7b226f223a3835317d0a7b226f223a3835327d0a7b226f223a3835337d0a7b226f223a3835347d0a7b226f223a3835357d0a7b226f223a3835367d0a7b226f223a3835377d0a7b226f223a3835387d0a7b226f223a3835397d0a7b226f223a3836307d0a7b226f223a3836317d0a7b226f223a3836327d0a7b226f223a3836337d0a7b226f223a3836347d0a7b226f223a3836357d0a7b226f223a3836367d0a7b226f223a3836377d0a7b226f223a3836387d0a7b226f223a3836397d0a7b226f223a3837307d0a7b226f223a3837317d0a7b226f223a3837327d0a7b226f223a3837337d0a7b226f223a3837347d0a7b226f223a3837357d0a7b226f223a3837367d0a7b226f223a3837377d0a7b226f223a3837387d0a7b226f223a3837397d0a7b226f223a3838307d0a7b226f223a3838317d0a7b226f223a3838327d0a7b226f223a3838337d0a7b226f223a3838347d0a7b226f223a3838357d0a7b226f223a3838367d0a7b226f223a3838377d0a7b226f223a3838387d0a7b226f223a3838397d0a7b226f223a3839307d0a7b226f223a3839317d0a7b226f223a3839327d0a7b226f223a3839337d0a7b226f223a3839347d0a7b226f223a3839357d0a7b226f223a3839367d0a7b226f223a3839377d0a7b226f223a3839387d0a7b226f223a3839397d0a7b226f223a3930307d0a7b226f223a3930317d0a7b226f223a3930327d0a7b226f223a3930337d0a7b226f223a3930347d0a7b226f223a3930357d0a7b226f223a3930367d0a7b226f223a3930377d0a7b226f223a3930387d0a7b226f223a3930397d0a7b226f223a3931307d0a7b226f223a3931317d0a7b226f223a3931327d0a7b226f223a3931337d0a7b226f223a3931347d0a7b226f223a3931357d0a7b226f223a3931367d0a7b226f223a3931377d0a7b226f223a3931387d0a7b226f223a3931397d0a7b226f223a3932307d0a7b226f223a3932317d0a7b226f223a3932327d0a7b226f223a3932337d0a7b226f223a3932347d0a7b226f223a3932357d0a7b226f223a3932367d0a7b226f223a3932377d0a7b226f223a3932387d0a7b226f223a3932397d0a7b226f223a3933307d0a7b226f223a3933317d0a7b226f223a3933327d0a7b226f223a3933337d0a7b226f223a3933347d0a7b226f223a3933357d0a7b226f223a3933367d0a7b226f223a3933377d0a7b226f223a3933387d0a7b226f223a3933397d0a7b226f223a3934307d0a7b226f223a3934317d0a7b226f223a3934327d0a7b226f223a3934337d0a7b226f223a3934347d0a7b226f223a3934357d0a7b226f223a3934367d0a7b226f223a3934377d0a7b226f223a3934387d0a7b226f223a3934397d0a7b226f223a3935307d0a7b226f223a3935317d0a7b226f223a3935327d0a7b226f223a3935337d0a7b226f223a3935347d0a7b226f223a3935357d0a7b226f223a3935367d0a7b226f223a3935377d0a7b226f223a3935387d0a7b226f223a3935397d0a7b226f223a3936307d0a7b226f223a3936317d0a7b226f223a3936327d0a7b226f223a3936337d0a7b226f223a3936347d0a7b226f223a3936357d0a7b226f223a3936367d0a7b226f223a3936377d0a7b226f223a3936387d0a7b226f223a3936397d0a7b226f223a3937307d0a7b226f223a3937317d0a7b226f223a3937327d0a7b226f223a3937337d0a7b226f223a3937347d0a7b226f223a3937357d0a7b226f223a3937367d0a7b226f223a3937377d0a7b226f223a3937387d0a7b226f223a3937397d0a7b226f223a3938307d0a7b226f223a3938317d0a7b226f223a3938327d0a7b226f223a3938337d0a7b226f223a3938347d0a7b226f223a3938357d0a7b226f223a3938367d0a7b226f223a3938377d0a7b226f223a3938387d0a7b226f223a3938397d0a7b226f223a3939307d0a7b226f223a3939317d0a7b226f223a3939327d0a7b226f223a3939337d0a7b226f223a3939347d0a7b226f223a3939357d0a7b226f223a3939367d0a7b226f223a3939377d0a7b226f223a3939387d0a7b226f223a3939397d0a7b226f223a313030307d0a","obligation_id":"robl_interop","sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277"}, + {"account_id":"acct_interop","body_hex":"7b2261223a322c22636f6e74726f6c223a225c75303030375c6e5c225c5c222c226e616d65223a22636166c3a920f09f988020e697a5e69cace8aa9e222c22736570223a22e280a8222c227a223a317d0a7b22666c6f6174223a302e33303030303030303030303030303030342c226d61785f696e74223a393030373139393235343734303939312c226e223a302c226e6567223a2d31322e352c226e6573746564223a7b2261223a5b5d2c227a223a5b312c7b2261223a747275652c2262223a6e756c6c7d5d7d2c2274696e79223a31652d377d0a","obligation_id":"robl_interop","sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}, + {"account_id":"acct_interop","body_hex":"7b2264617465223a22323032362d31302d3031222c22696d7072657373696f6e73223a313230302c226d656469615f6275795f6964223a226d625f31222c227061636b6167655f6964223a22706b675f31222c227370656e64223a2234322e3137227d0a","obligation_id":"robl_interop","sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"} ], "objects": { - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/0aca7cfc22e75fff0d7a566b8ddddf8cc5b336889cf87f72205bec7206137cee/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz": "H4sIAAAAAAAAAyXKQQqAIBBA0X3HmLXFjESBlxHNQUQ0yVpEdPeylv/xL3BmZ1AgUU49YY8EAkIqG9ca1lxBkUQUkNgFo+1x6uDePVndxmKWaDz/VqL/sBbOrUc50Ax39wBBEPOlZAAAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/27e955cd7fbfa6a59c45ae49c074730a25eccd701bfb8c96e296135f5c9df2fd/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz": "H4sIAAAAAAAAA03aQbIsJw5A0bmX8VdQgARSb8oLcHjv3R1RHGqUsyvH9zsFCfnPn7///Ofz71///P85vs/5fa7vM77P/D7393m+z/o++3YEb3Hc5LjNcaPjVsfNjtsdNzxued7y9N96y/OW5y3PW563PG953vK85XXL65aXf4ZbXre8bnnd8rrldcvrluOW45bjlsO/8C3HLcctxy3HLcct5y3nLect5y2n/3m3nLect5y3nLe8b3nf8r7lfcv7lre/i1vet7xved/yueVzy+eWzy2fWz63fPzJ3fK55XPLdct1y3XLdct1y3XLdcvlr/mW65b7lvuW+5b7lvuW+5b7lvuWG5QnBZUPKx9YPrR8cPnw8gHmQ8wHmY8ZPxzNeCCfyEfymXwon8rHkssB5pjPvBlsDjgHnQPPwecAdBA6EB2MjvV+WMzAdHA6QB2kDlQHqwPWQevAdcT79TKD2IHsYHZAO6gd2A5uB7iD3JHvJ9IMeAe9A9/B7wB4EDwQHgwPiMd+v8NmcDxAHiQPlAfLA+ZB88B58DzO+7E3A+nB9IB6UD2wHlwPsAfZA+1Rb0Uxg+6B9+B7AD4IH4gPxgfkg/LRb9l665aFi/PJ+eR8cj45n5xPzifnk/M53uJoBueT88n55HxyPjmfnM+3/r4F+GcFNuOtwW8RfqvwW4bfOvwWYs4n55Pzud4ybwbnk/PJ+eR8cj45n5xPzifnM95ewgzOJ+eT88n55HxyPjmfnE/OZ74NixmcT84n55PzyfnkfHI+OZ+cz/12RWZwPjmfnE/OJ+eT88n55HxyPs/bepnB+eR8cj45n5xPzifnk/PJ+ay3vzOD88n55HxyPjmfnE/OJ+eT89lvE/l2kbaRnC/OF+eL88X54nxxvjhfnK/xtqpmcL44X5wvzhfni/PF+eJ8cb7m2w+bwfnifHG+OF+cL87X23G/Lffbc/9sus142+63734b77fzfltvzhfni/PF+Yq3szeD88X54nxxvjhfnC/OF+eL85Xv9cEMzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PFeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfn8d6x30v2e8t+r9k/79lmvDft96r93rXfyzbnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwnpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpznO1V7x2rvXO0drL2TtZ+jNTPe4do7XXvHa5wn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vz/c7R30H6O0l/R+nvLP0dpv+cppvxztPfgTrnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8vJuzd3X27s7e5dm7PXvXZ+/+7OcCzYx3hcb54fxwfjg/nB/OD+eH88P54fxwfjg/nB/OD+eH88P54fxwfjg/nBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV7vrvxdlr/b8ndd/u7L34X5uzF/V+Y/d+ZmcF6cF+fFeXFenBfnxXlxXpwX5815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeb+vY97nMe/7mPeBzPtC5n0i876ReR/JvK9kfj6T+flO5n9D/gsn4sd0rSYAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/480d5636d1a3d4eea19ed0915558ba0a53c23f9b8e9705235872e6f4350cf761/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz": "H4sIAAAAAAAAA1WOQQrCMBBF9z1F+etUktpSkqsYkaipCDUVTUEtBW+hC9GVoOItvImL4tIjOLpzFsP/8xner2GgYoZR6fyiLKCgK855pp2G1mBwZmbpOjL54xq+T/tt2O7O7eH+uh0pXdo5hc/thfQGSjRBjbwojYfinS7/n4RhZlaDqaNUEkNIGadJlnApBYHohbadQEUi7qRfvfR2DFV/O/b6P0JPsJ/1i8oyDKFcVRRNv2HwU7emBjbKmuAD5D6qgNYAAAA=", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/5a75b70aa480c4eb2b21387f3f727c4ea85ef87b86f93f2b05aa54ac48cd3a66/b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8/000000.jsonl.gz": "H4sIAAAAAAAAAyXKQQqAIBBA0X3HmLXFjESBlxHNQUQ0yVpEdPeylv/xL3BmZ1AgUU49YY8EAkIqG9ca1lxBkUQUkNgFo+1x6uDePVndxmKWaDz/VqL/sBbOrUc50Ax39wBBEPOlZAAAAA==", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/76fd73e617881fd6a8038570f07fe2a82bb95306474cfa6ccbd38781110ec845/9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869/000000.jsonl.gz": "H4sIAAAAAAAAA1WOQQrCMBBF9z1F+etUktpSkqsYkaipCDUVTUEtBW+hC9GVoOItvImL4tIjOLpzFsP/8xner2GgYoZR6fyiLKCgK855pp2G1mBwZmbpOjL54xq+T/tt2O7O7eH+uh0pXdo5hc/thfQGSjRBjbwojYfinS7/n4RhZlaDqaNUEkNIGadJlnApBYHohbadQEUi7qRfvfR2DFV/O/b6P0JPsJ/1i8oyDKFcVRRNv2HwU7emBjbKmuAD5D6qgNYAAAA=", - "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/9bf8b4413d524f9a77782d9cc7ecc980e03bdf15799c53861169030784f56419/a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b/000000.jsonl.gz": "H4sIAAAAAAAAA03aQbIsJw5A0bmX8VdQgARSb8oLcHjv3R1RHGqUsyvH9zsFCfnPn7///Ofz71///P85vs/5fa7vM77P/D7393m+z/o++3YEb3Hc5LjNcaPjVsfNjtsdNzxued7y9N96y/OW5y3PW563PG953vK85XXL65aXf4ZbXre8bnnd8rrldcvrluOW45bjlsO/8C3HLcctxy3HLcct5y3nLect5y2n/3m3nLect5y3nLe8b3nf8r7lfcv7lre/i1vet7xved/yueVzy+eWzy2fWz63fPzJ3fK55XPLdct1y3XLdct1y3XLdcvlr/mW65b7lvuW+5b7lvuW+5b7lvuWG5QnBZUPKx9YPrR8cPnw8gHmQ8wHmY8ZPxzNeCCfyEfymXwon8rHkssB5pjPvBlsDjgHnQPPwecAdBA6EB2MjvV+WMzAdHA6QB2kDlQHqwPWQevAdcT79TKD2IHsYHZAO6gd2A5uB7iD3JHvJ9IMeAe9A9/B7wB4EDwQHgwPiMd+v8NmcDxAHiQPlAfLA+ZB88B58DzO+7E3A+nB9IB6UD2wHlwPsAfZA+1Rb0Uxg+6B9+B7AD4IH4gPxgfkg/LRb9l665aFi/PJ+eR8cj45n5xPzifnk/M53uJoBueT88n55HxyPjmfnM+3/r4F+GcFNuOtwW8RfqvwW4bfOvwWYs4n55Pzud4ybwbnk/PJ+eR8cj45n5xPzifnM95ewgzOJ+eT88n55HxyPjmfnE/OZ74NixmcT84n55PzyfnkfHI+OZ+cz/12RWZwPjmfnE/OJ+eT88n55HxyPs/bepnB+eR8cj45n5xPzifnk/PJ+ay3vzOD88n55HxyPjmfnE/OJ+eT89lvE/l2kbaRnC/OF+eL88X54nxxvjhfnK/xtqpmcL44X5wvzhfni/PF+eJ8cb7m2w+bwfnifHG+OF+cL87X23G/Lffbc/9sus142+63734b77fzfltvzhfni/PF+Yq3szeD88X54nxxvjhfnC/OF+eL85Xv9cEMzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PFeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfn8d6x30v2e8t+r9k/79lmvDft96r93rXfyzbnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwnpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpznO1V7x2rvXO0drL2TtZ+jNTPe4do7XXvHa5wn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vz/c7R30H6O0l/R+nvLP0dpv+cppvxztPfgTrnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8vJuzd3X27s7e5dm7PXvXZ+/+7OcCzYx3hcb54fxwfjg/nB/OD+eH88P54fxwfjg/nB/OD+eH88P54fxwfjg/nBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV7vrvxdlr/b8ndd/u7L34X5uzF/V+Y/d+ZmcF6cF+fFeXFenBfnxXlxXpwX5815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeb+vY97nMe/7mPeBzPtC5n0i876ReR/JvK9kfj6T+flO5n9D/gsn4sd0rSYAAA==" + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/0aca7cfc22e75fff0d7a566b8ddddf8cc5b336889cf87f72205bec7206137cee/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz":"H4sIAAAAAAAAAyXKQQqAIBBA0X3HmLXFjESBlxHNQUQ0yVpEdPeylv/xL3BmZ1AgUU49YY8EAkIqG9ca1lxBkUQUkNgFo+1x6uDePVndxmKWaDz/VqL/sBbOrUc50Ax39wBBEPOlZAAAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/27e955cd7fbfa6a59c45ae49c074730a25eccd701bfb8c96e296135f5c9df2fd/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz":"H4sIAAAAAAAAA03aQbIsJw5A0bmX8VdQgARSb8oLcHjv3R1RHGqUsyvH9zsFCfnPn7///Ofz71///P85vs/5fa7vM77P/D7393m+z/o++3YEb3Hc5LjNcaPjVsfNjtsdNzxued7y9N96y/OW5y3PW563PG953vK85XXL65aXf4ZbXre8bnnd8rrldcvrluOW45bjlsO/8C3HLcctxy3HLcct5y3nLect5y2n/3m3nLect5y3nLe8b3nf8r7lfcv7lre/i1vet7xved/yueVzy+eWzy2fWz63fPzJ3fK55XPLdct1y3XLdct1y3XLdcvlr/mW65b7lvuW+5b7lvuW+5b7lvuWG5QnBZUPKx9YPrR8cPnw8gHmQ8wHmY8ZPxzNeCCfyEfymXwon8rHkssB5pjPvBlsDjgHnQPPwecAdBA6EB2MjvV+WMzAdHA6QB2kDlQHqwPWQevAdcT79TKD2IHsYHZAO6gd2A5uB7iD3JHvJ9IMeAe9A9/B7wB4EDwQHgwPiMd+v8NmcDxAHiQPlAfLA+ZB88B58DzO+7E3A+nB9IB6UD2wHlwPsAfZA+1Rb0Uxg+6B9+B7AD4IH4gPxgfkg/LRb9l665aFi/PJ+eR8cj45n5xPzifnk/M53uJoBueT88n55HxyPjmfnM+3/r4F+GcFNuOtwW8RfqvwW4bfOvwWYs4n55Pzud4ybwbnk/PJ+eR8cj45n5xPzifnM95ewgzOJ+eT88n55HxyPjmfnE/OZ74NixmcT84n55PzyfnkfHI+OZ+cz/12RWZwPjmfnE/OJ+eT88n55HxyPs/bepnB+eR8cj45n5xPzifnk/PJ+ay3vzOD88n55HxyPjmfnE/OJ+eT89lvE/l2kbaRnC/OF+eL88X54nxxvjhfnK/xtqpmcL44X5wvzhfni/PF+eJ8cb7m2w+bwfnifHG+OF+cL87X23G/Lffbc/9sus142+63734b77fzfltvzhfni/PF+Yq3szeD88X54nxxvjhfnC/OF+eL85Xv9cEMzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PFeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfn8d6x30v2e8t+r9k/79lmvDft96r93rXfyzbnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwnpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpznO1V7x2rvXO0drL2TtZ+jNTPe4do7XXvHa5wn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vz/c7R30H6O0l/R+nvLP0dpv+cppvxztPfgTrnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8vJuzd3X27s7e5dm7PXvXZ+/+7OcCzYx3hcb54fxwfjg/nB/OD+eH88P54fxwfjg/nB/OD+eH88P54fxwfjg/nBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV7vrvxdlr/b8ndd/u7L34X5uzF/V+Y/d+ZmcF6cF+fFeXFenBfnxXlxXpwX5815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeb+vY97nMe/7mPeBzPtC5n0i876ReR/JvK9kfj6T+flO5n9D/gsn4sd0rSYAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/480d5636d1a3d4eea19ed0915558ba0a53c23f9b8e9705235872e6f4350cf761/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz":"H4sIAAAAAAAAA1WOQQrCMBBF9z1F+etUktpSkqsYkaipCDUVTUEtBW+hC9GVoOItvImL4tIjOLpzFsP/8xner2GgYoZR6fyiLKCgK855pp2G1mBwZmbpOjL54xq+T/tt2O7O7eH+uh0pXdo5hc/thfQGSjRBjbwojYfinS7/n4RhZlaDqaNUEkNIGadJlnApBYHohbadQEUi7qRfvfR2DFV/O/b6P0JPsJ/1i8oyDKFcVRRNv2HwU7emBjbKmuAD5D6qgNYAAAA=", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/5a75b70aa480c4eb2b21387f3f727c4ea85ef87b86f93f2b05aa54ac48cd3a66/b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8/000000.jsonl.gz":"H4sIAAAAAAAAAyXKQQqAIBBA0X3HmLXFjESBlxHNQUQ0yVpEdPeylv/xL3BmZ1AgUU49YY8EAkIqG9ca1lxBkUQUkNgFo+1x6uDePVndxmKWaDz/VqL/sBbOrUc50Ax39wBBEPOlZAAAAA==", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/76fd73e617881fd6a8038570f07fe2a82bb95306474cfa6ccbd38781110ec845/9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869/000000.jsonl.gz":"H4sIAAAAAAAAA1WOQQrCMBBF9z1F+etUktpSkqsYkaipCDUVTUEtBW+hC9GVoOItvImL4tIjOLpzFsP/8xner2GgYoZR6fyiLKCgK855pp2G1mBwZmbpOjL54xq+T/tt2O7O7eH+uh0pXdo5hc/thfQGSjRBjbwojYfinS7/n4RhZlaDqaNUEkNIGadJlnApBYHohbadQEUi7qRfvfR2DFV/O/b6P0JPsJ/1i8oyDKFcVRRNv2HwU7emBjbKmuAD5D6qgNYAAAA=", + "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/9bf8b4413d524f9a77782d9cc7ecc980e03bdf15799c53861169030784f56419/a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b/000000.jsonl.gz":"H4sIAAAAAAAAA03aQbIsJw5A0bmX8VdQgARSb8oLcHjv3R1RHGqUsyvH9zsFCfnPn7///Ofz71///P85vs/5fa7vM77P/D7393m+z/o++3YEb3Hc5LjNcaPjVsfNjtsdNzxued7y9N96y/OW5y3PW563PG953vK85XXL65aXf4ZbXre8bnnd8rrldcvrluOW45bjlsO/8C3HLcctxy3HLcct5y3nLect5y2n/3m3nLect5y3nLe8b3nf8r7lfcv7lre/i1vet7xved/yueVzy+eWzy2fWz63fPzJ3fK55XPLdct1y3XLdct1y3XLdcvlr/mW65b7lvuW+5b7lvuW+5b7lvuWG5QnBZUPKx9YPrR8cPnw8gHmQ8wHmY8ZPxzNeCCfyEfymXwon8rHkssB5pjPvBlsDjgHnQPPwecAdBA6EB2MjvV+WMzAdHA6QB2kDlQHqwPWQevAdcT79TKD2IHsYHZAO6gd2A5uB7iD3JHvJ9IMeAe9A9/B7wB4EDwQHgwPiMd+v8NmcDxAHiQPlAfLA+ZB88B58DzO+7E3A+nB9IB6UD2wHlwPsAfZA+1Rb0Uxg+6B9+B7AD4IH4gPxgfkg/LRb9l665aFi/PJ+eR8cj45n5xPzifnk/M53uJoBueT88n55HxyPjmfnM+3/r4F+GcFNuOtwW8RfqvwW4bfOvwWYs4n55Pzud4ybwbnk/PJ+eR8cj45n5xPzifnM95ewgzOJ+eT88n55HxyPjmfnE/OZ74NixmcT84n55PzyfnkfHI+OZ+cz/12RWZwPjmfnE/OJ+eT88n55HxyPs/bepnB+eR8cj45n5xPzifnk/PJ+ay3vzOD88n55HxyPjmfnE/OJ+eT89lvE/l2kbaRnC/OF+eL88X54nxxvjhfnK/xtqpmcL44X5wvzhfni/PF+eJ8cb7m2w+bwfnifHG+OF+cL87X23G/Lffbc/9sus142+63734b77fzfltvzhfni/PF+Yq3szeD88X54nxxvjhfnC/OF+eL85Xv9cEMzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PF+eJ8cb44X5wvzhfni/PFeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfn8d6x30v2e8t+r9k/79lmvDft96r93rXfyzbnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwHpwH58F5cB6cB+fBeXAenAfnwXlwnpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpznO1V7x2rvXO0drL2TtZ+jNTPe4do7XXvHa5wn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cJ+fJeXKenCfnyXlynpwn58l5cp6cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vzzfnmfHO+Od+cb84355vz/c7R30H6O0l/R+nvLP0dpv+cppvxztPfgTrnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535xvzjfnm/PN+eZ8c74535wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8cH44P5wfzg/nh/PD+eH8vJuzd3X27s7e5dm7PXvXZ+/+7OcCzYx3hcb54fxwfjg/nB/OD+eH88P54fxwfjg/nB/OD+eH88P54fxwfjg/nBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV6cF+fFeXFenBfnxXlxXpwX58V5cV7vrvxdlr/b8ndd/u7L34X5uzF/V+Y/d+ZmcF6cF+fFeXFenBfnxXlxXpwX5815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeXPenDfnzXlz3pw35815c96cN+fNeb+vY97nMe/7mPeBzPtC5n0i876ReR/JvK9kfj6T+flO5n9D/gsn4sd0rSYAAA==" }, "row_chunks": [ - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1, - "row_set_id": "rrev_py_single-row_adjustment_inline", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/0aca7cfc22e75fff0d7a566b8ddddf8cc5b336889cf87f72205bec7206137cee/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "451e665e46f08ecbe80a24afde05a61b5762dc57aee04e611382fc022277ecc5", - "row_count": 1, - "row_set_id": "rrev_py_single-row_adjustment_reports", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": 106 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1, - "row_set_id": "rrev_py_single-row_revision_inline", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 100, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/5a75b70aa480c4eb2b21387f3f727c4ea85ef87b86f93f2b05aa54ac48cd3a66/b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "451e665e46f08ecbe80a24afde05a61b5762dc57aee04e611382fc022277ecc5", - "row_count": 1, - "row_set_id": "rrev_py_single-row_revision_reports", - "segments": [ - { - "byte_count": 100, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 1, - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336" - } - ], - "sha256": "8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336", - "size_bytes": 106 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1001, - "row_set_id": "rrev_py_three-segments_adjustment_inline", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/27e955cd7fbfa6a59c45ae49c074730a25eccd701bfb8c96e296135f5c9df2fd/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "8ebd7f074937968afb03873bb41d081da29607404639eb28dbc6a833b88f588b", - "row_count": 1001, - "row_set_id": "rrev_py_three-segments_adjustment_reports", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": 1987 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 1001, - "row_set_id": "rrev_py_three-segments_revision_inline", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 9901, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/9bf8b4413d524f9a77782d9cc7ecc980e03bdf15799c53861169030784f56419/a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "8ebd7f074937968afb03873bb41d081da29607404639eb28dbc6a833b88f588b", - "row_count": 1001, - "row_set_id": "rrev_py_three-segments_revision_reports", - "segments": [ - { - "byte_count": 4890, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 500, - "sha256": "6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502" - }, - { - "byte_count": 5000, - "byte_offset": 4890, - "first_ordinal": 500, - "row_count": 500, - "sha256": "18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9" - }, - { - "byte_count": 11, - "byte_offset": 9890, - "first_ordinal": 1000, - "row_count": 1, - "sha256": "34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f" - } - ], - "sha256": "0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277", - "size_bytes": 1987 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 2, - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_inline", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/480d5636d1a3d4eea19ed0915558ba0a53c23f9b8e9705235872e6f4350cf761/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "3ec156c72bc93f75c35a90f228102774d1e9350cbee607554413bb1d38e9d2fb", - "row_count": 2, - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_reports", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": 200 - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "none", - "first_ordinal": 0, - "object_ref": null, - "obligation_id": "robl_interop", - "physical_sha256": null, - "row_count": 2, - "row_set_id": "rrev_py_unicode-and-numbers_revision_inline", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": null - }, - { - "account_id": "acct_interop", - "canonical_size_bytes": 214, - "chunk_index": 0, - "compression": "gzip", - "first_ordinal": 0, - "object_ref": "adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/76fd73e617881fd6a8038570f07fe2a82bb95306474cfa6ccbd38781110ec845/9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869/000000.jsonl.gz", - "obligation_id": "robl_interop", - "physical_sha256": "3ec156c72bc93f75c35a90f228102774d1e9350cbee607554413bb1d38e9d2fb", - "row_count": 2, - "row_set_id": "rrev_py_unicode-and-numbers_revision_reports", - "segments": [ - { - "byte_count": 214, - "byte_offset": 0, - "first_ordinal": 0, - "row_count": 2, - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023" - } - ], - "sha256": "10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023", - "size_bytes": 200 - } + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1,"row_set_id":"rrev_py_single-row_adjustment_inline","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/0aca7cfc22e75fff0d7a566b8ddddf8cc5b336889cf87f72205bec7206137cee/5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"451e665e46f08ecbe80a24afde05a61b5762dc57aee04e611382fc022277ecc5","row_count":1,"row_set_id":"rrev_py_single-row_adjustment_reports","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":106}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1,"row_set_id":"rrev_py_single-row_revision_inline","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":100,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/5a75b70aa480c4eb2b21387f3f727c4ea85ef87b86f93f2b05aa54ac48cd3a66/b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"451e665e46f08ecbe80a24afde05a61b5762dc57aee04e611382fc022277ecc5","row_count":1,"row_set_id":"rrev_py_single-row_revision_reports","segments":[{"byte_count":100,"byte_offset":0,"first_ordinal":0,"row_count":1,"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336"}],"sha256":"8fcc0c1b57966f5ab49d8bed4782722734119ae1875a761550209e87a6d2a336","size_bytes":106}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1001,"row_set_id":"rrev_py_three-segments_adjustment_inline","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/27e955cd7fbfa6a59c45ae49c074730a25eccd701bfb8c96e296135f5c9df2fd/49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"8ebd7f074937968afb03873bb41d081da29607404639eb28dbc6a833b88f588b","row_count":1001,"row_set_id":"rrev_py_three-segments_adjustment_reports","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":1987}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":1001,"row_set_id":"rrev_py_three-segments_revision_inline","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":9901,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/9bf8b4413d524f9a77782d9cc7ecc980e03bdf15799c53861169030784f56419/a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"8ebd7f074937968afb03873bb41d081da29607404639eb28dbc6a833b88f588b","row_count":1001,"row_set_id":"rrev_py_three-segments_revision_reports","segments":[{"byte_count":4890,"byte_offset":0,"first_ordinal":0,"row_count":500,"sha256":"6bb5ee41da32436cb1c9eabe9222133864103cb34357f8428d11d37c5f0c4502"},{"byte_count":5000,"byte_offset":4890,"first_ordinal":500,"row_count":500,"sha256":"18081778436f618fa78cd93f348daffd4f8b32146cb2a46a09f0d48118b178d9"},{"byte_count":11,"byte_offset":9890,"first_ordinal":1000,"row_count":1,"sha256":"34372091671ff2e0982e59df385c1e928082db35c3e288a575831498e24bf21f"}],"sha256":"0779a24363382975e70ba1fa57ec9114d97cbba03a85253f7c3c776b29e07277","size_bytes":1987}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":2,"row_set_id":"rrev_py_unicode-and-numbers_adjustment_inline","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/adjustment/480d5636d1a3d4eea19ed0915558ba0a53c23f9b8e9705235872e6f4350cf761/628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"3ec156c72bc93f75c35a90f228102774d1e9350cbee607554413bb1d38e9d2fb","row_count":2,"row_set_id":"rrev_py_unicode-and-numbers_adjustment_reports","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":200}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"none","first_ordinal":0,"object_ref":null,"obligation_id":"robl_interop","physical_sha256":null,"row_count":2,"row_set_id":"rrev_py_unicode-and-numbers_revision_inline","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":null}, + {"account_id":"acct_interop","canonical_size_bytes":214,"chunk_index":0,"compression":"gzip","first_ordinal":0,"object_ref":"adcp-rows/1b6d0668f0d40a94bdd9b9c938602ec73c7fb4a007cd24495b088f6780f3e3ae/50fab37758f9ad43d91228fecc9bb96e0154f20e4e997d142e08890e1c40796d/2026-09-30/snapshot/76fd73e617881fd6a8038570f07fe2a82bb95306474cfa6ccbd38781110ec845/9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869/000000.jsonl.gz","obligation_id":"robl_interop","physical_sha256":"3ec156c72bc93f75c35a90f228102774d1e9350cbee607554413bb1d38e9d2fb","row_count":2,"row_set_id":"rrev_py_unicode-and-numbers_revision_reports","segments":[{"byte_count":214,"byte_offset":0,"first_ordinal":0,"row_count":2,"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023"}],"sha256":"10799084bfe8f8fc94f0d61a4df1c6dc752f14fe804b8c8baa92392faac15023","size_bytes":200} ], "row_sets": [ - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_py_single-row_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187", - "content_size_bytes": 101, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_py_single-row_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "f04cd965936bf4821f54148bcaa037a7a9b4f0315e7d9cead0b6e90baed2f818", - "content_size_bytes": 344, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_py_single-row_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8", - "content_size_bytes": 345, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1, - "row_manifest_sha256": "8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a", - "row_set_id": "rrev_py_single-row_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_py_three-segments_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe", - "content_size_bytes": 9902, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_py_three-segments_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "cd76d91d96b99808a64c0a9348d81d82584153af22ea74d1138970381705f57d", - "content_size_bytes": 10152, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_py_three-segments_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b", - "content_size_bytes": 10153, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 1001, - "row_manifest_sha256": "c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927", - "row_set_id": "rrev_py_three-segments_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_inline", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095", - "content_size_bytes": 215, - "digest_profile": "rows_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_py_unicode-and-numbers_adjustment_reports", - "row_set_kind": "adjustment", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "795bc2f0c2979e4e3178d994692f994f34220bca84af68db33705ac6fd52efdd", - "content_size_bytes": 467, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "inline", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_py_unicode-and-numbers_revision_inline", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - }, - { - "account_id": "acct_interop", - "algorithm": "adcp_jcs_rows_v1", - "chunk_count": 1, - "content_sha256": "9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869", - "content_size_bytes": 468, - "digest_profile": "revision_envelope_v1", - "format": "jsonl", - "obligation_id": "robl_interop", - "row_binding_id": "reports", - "row_count": 2, - "row_manifest_sha256": "a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f", - "row_set_id": "rrev_py_unicode-and-numbers_revision_reports", - "row_set_kind": "revision", - "rows_shared_from_row_set_id": null - } + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_py_single-row_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"5768a21e0e903063a5b0158a5053c240289f1ac1e563957fa5fc8915c2ffc187","content_size_bytes":101,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_py_single-row_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"f04cd965936bf4821f54148bcaa037a7a9b4f0315e7d9cead0b6e90baed2f818","content_size_bytes":344,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_py_single-row_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"b7f659d4967acd5e1d55aaee8fd97a1e18d197a610b6c994c2bc16cc718302a8","content_size_bytes":345,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1,"row_manifest_sha256":"8d8a4a20352022452e253c68b47297a5f5ef0807881e4e7f042f76efab13577a","row_set_id":"rrev_py_single-row_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_py_three-segments_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"49cea3ff67422f369aef062db1faf3aa541ef88aa47cf36369b894c891e02cfe","content_size_bytes":9902,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_py_three-segments_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"cd76d91d96b99808a64c0a9348d81d82584153af22ea74d1138970381705f57d","content_size_bytes":10152,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_py_three-segments_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"a87c08703c1ac542c704086ada8372ba78925a4f1e6f008d8556a92b9d9c7f2b","content_size_bytes":10153,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":1001,"row_manifest_sha256":"c7c02b87deb344ae06f63be3b37189538b0c1c7f53035e6699c8d83733b92927","row_set_id":"rrev_py_three-segments_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_py_unicode-and-numbers_adjustment_inline","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"628122e5320202b9d578bfac11857240e5c0826ea8c30ae5034c4b90b7bfe095","content_size_bytes":215,"digest_profile":"rows_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_py_unicode-and-numbers_adjustment_reports","row_set_kind":"adjustment","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"795bc2f0c2979e4e3178d994692f994f34220bca84af68db33705ac6fd52efdd","content_size_bytes":467,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"inline","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_py_unicode-and-numbers_revision_inline","row_set_kind":"revision","rows_shared_from_row_set_id":null}, + {"account_id":"acct_interop","algorithm":"adcp_jcs_rows_v1","chunk_count":1,"content_sha256":"9da4eb545db09a88341b27b05d81247eae4bfeea407c4bdc4a57f16f3b497869","content_size_bytes":468,"digest_profile":"revision_envelope_v1","format":"jsonl","obligation_id":"robl_interop","row_binding_id":"reports","row_count":2,"row_manifest_sha256":"a31c73bc5f9f0b05b5496585647521898d33dc9e9e490a5efea6146e05049b6f","row_set_id":"rrev_py_unicode-and-numbers_revision_reports","row_set_kind":"revision","rows_shared_from_row_set_id":null} ] } } diff --git a/tests/test_normalize_pyproject_prerelease.py b/tests/test_normalize_pyproject_prerelease.py index 64329e7c2..48407b8dd 100644 --- a/tests/test_normalize_pyproject_prerelease.py +++ b/tests/test_normalize_pyproject_prerelease.py @@ -88,6 +88,43 @@ def github(path: str, token: str, *, body: dict | None = None) -> dict: ) +def test_release_pr_normalization_handles_the_storage_component_pr( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Each component's PR normalizes its own pyproject.toml; a duplicate branch is refused.""" + calls: list[tuple[str, dict | None]] = [] + branches = { + "pulls/1": "release-please--branches--main--components--adcp", + "pulls/2": "release-please--branches--main--components--adcp-reporting-storage", + } + + def github(path: str, token: str, *, body: dict | None = None) -> dict: + calls.append((path, body)) + if path in branches: + return { + "base": {"ref": "main", "repo": {"full_name": normalizer._REPOSITORY}}, + "head": {"ref": branches[path], "repo": {"full_name": normalizer._REPOSITORY}}, + } + if path.startswith("contents/"): + return { + "content": base64.b64encode(b'[project]\nversion = "0.1.0-beta.1"\n').decode(), + "sha": "blob", + } + return {} + + monkeypatch.setattr(normalizer, "_github", github) + normalizer.normalize_release_prs([{"number": 1}, {"number": 2}], "app-token") + writes = [(path, body["branch"]) for path, body in calls if body is not None] + assert writes == [ + ("contents/pyproject.toml", branches["pulls/1"]), + ("contents/packages/adcp-reporting-storage/pyproject.toml", branches["pulls/2"]), + ] + with pytest.raises(ValueError, match="unexpected release PR identity"): + normalizer.normalize_release_prs([{"number": 2}, {"number": 2}], "app-token") + with pytest.raises(ValueError, match="more release PRs"): + normalizer.normalize_release_prs([{"number": 1}] * 4, "app-token") + + def test_release_pr_normalization_refuses_foreign_pr(monkeypatch: pytest.MonkeyPatch) -> None: def github(path: str, token: str, *, body: dict | None = None) -> dict: return { diff --git a/tests/test_release_configuration.py b/tests/test_release_configuration.py index 902238d82..62df1004d 100644 --- a/tests/test_release_configuration.py +++ b/tests/test_release_configuration.py @@ -35,3 +35,41 @@ def test_release_please_keeps_sdk_9_on_the_beta_channel() -> None: assert package["versioning"] == "prerelease" assert package["prerelease-type"] == "beta" assert package["prerelease"] is True + + +STORAGE = ROOT / "packages/adcp-reporting-storage" + + +def test_storage_distribution_version_matches_its_manifest_entry() -> None: + """adcp-reporting-storage is its own Release Please component on the beta channel.""" + pyproject = (STORAGE / "pyproject.toml").read_text() + project_section = re.search( + r'^\[project\]\s*$.*?^version\s*=\s*"([^"]+)"', + pyproject, + flags=re.MULTILINE | re.DOTALL, + ) + assert project_section is not None + manifest = json.loads((ROOT / ".release-please-manifest.json").read_text()) + assert project_section.group(1) == pep440_prerelease( + manifest["packages/adcp-reporting-storage"] + ) + config = json.loads((ROOT / "release-please-config.json").read_text()) + package = config["packages"]["packages/adcp-reporting-storage"] + assert package["component"] == package["package-name"] == "adcp-reporting-storage" + assert package["versioning"] == "prerelease" + assert package["prerelease-type"] == "beta" + assert package["prerelease"] is True + # Its tags carry the component name; the root keeps bare vX.Y.Z tags. + assert package["include-component-in-tag"] is True + assert config["packages"]["."]["include-component-in-tag"] is False + # Two components, two release PRs: the normalizer and the publish queue + # are written for that. + assert config["separate-pull-requests"] is True + # Release Please hands the root path every commit unless told otherwise; + # without this a storage-only fix or breaking change would release adcp. + assert config["packages"]["."]["exclude-paths"] == ["packages/adcp-reporting-storage"] + + +def test_storage_distribution_pins_one_adcp_major() -> None: + pyproject = (STORAGE / "pyproject.toml").read_text() + assert re.search(r'"adcp>=9\.0\.0(?:b\d+)?,<10"', pyproject), "adcp range must be one major" diff --git a/tests/test_reporting_ledger.py b/tests/test_reporting_ledger.py index 2100e5058..e6dc209e4 100644 --- a/tests/test_reporting_ledger.py +++ b/tests/test_reporting_ledger.py @@ -1462,8 +1462,9 @@ def test_the_identity_digest_omits_an_absent_mismatch_code() -> None: # retry would then fail with STATUS_IDENTITY_CONFLICT instead of replaying # as `unchanged` -- the one thing an idempotent append-only surface must # never do. + from adcp_reporting_storage.ledger.pg import _consumer_status_payload + from adcp.reporting.ledger.models import ConsumerStatusRecord - from adcp.reporting.ledger.pg import _consumer_status_payload from adcp.reporting.ledger.store import _consumer_status_identity def record(**overrides): diff --git a/tests/test_reporting_period_date.py b/tests/test_reporting_period_date.py index 4e2447747..23237a29e 100644 --- a/tests/test_reporting_period_date.py +++ b/tests/test_reporting_period_date.py @@ -10,14 +10,14 @@ from typing import Any import pytest - -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( REPORTING_ROW_DEFAULT_KEY_TEMPLATE, render_reporting_row_object_key_v1, reporting_period_date_v1, ) +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError + date = reporting_period_date_v1 diff --git a/tests/test_reporting_production_maintenance.py b/tests/test_reporting_production_maintenance.py index e4237ff3e..d814590a6 100644 --- a/tests/test_reporting_production_maintenance.py +++ b/tests/test_reporting_production_maintenance.py @@ -11,13 +11,25 @@ class Store: + """A store listing the retention passes it opted into, as the shipped stores do.""" + def __init__(self, *, rows: bool, feed: bool, fail: str | None = None) -> None: - self._row_storage = object() if rows else None - self._change_feed = feed + self.rows, self.feed = rows, feed self.calls: list[str] = [] self.fail = fail self.report: Any = None + def maintenance_operations(self) -> tuple[tuple[str, Any], ...]: + operations: list[tuple[str, Any]] = [] + if self.rows: + operations.append(("intent_sweep", lambda: self.sweep_row_write_intents(limit=100))) + operations.append(("row_pruning", lambda: self.prune_expired_row_sets(limit=20))) + if self.rows or self.feed: + operations.append(("period_retirement", lambda: self.retire_expired_periods(limit=20))) + if self.feed: + operations.append(("change_pruning", lambda: self.prune_changes())) + return tuple(operations) + async def _record(self, name: str) -> None: self.calls.append(name) if self.fail == name: @@ -48,6 +60,28 @@ def service(store: Any, seconds: float | None = 300.0) -> ReportingProductionSup return instance +def test_postgres_store_lists_the_passes_for_its_features() -> None: + """The selection lives in the store; the worker runs whatever it lists.""" + pytest.importorskip("psycopg") # the store refuses to construct without its driver + from adcp_reporting_storage.ledger.pg import PgReportingLedgerStore + + cases = [ + ( + dict(row_storage=True, change_feed=True), + ["intent_sweep", "row_pruning", "period_retirement", "change_pruning"], + ), + ( + dict(row_storage=True, change_feed=False), + ["intent_sweep", "row_pruning", "period_retirement"], + ), + (dict(row_storage=False, change_feed=True), ["period_retirement", "change_pruning"]), + (dict(row_storage=False, change_feed=False), []), + ] + for options, expected in cases: + store = PgReportingLedgerStore(pool=object(), **options) # type: ignore[arg-type] + assert [name for name, _ in store.maintenance_operations()] == expected, options + + async def test_maintenance_runs_only_what_the_store_opted_into() -> None: cases = [ (dict(rows=True, feed=True), ["sweep", "prune_rows", "retire", "prune"]), diff --git a/tests/test_reporting_production_public.py b/tests/test_reporting_production_public.py index 87322a1b3..916bb9295 100644 --- a/tests/test_reporting_production_public.py +++ b/tests/test_reporting_production_public.py @@ -1,4 +1,4 @@ -"""All public exports work without optional drivers; concrete PG classes are lazy.""" +"""All public exports work without optional drivers; the PostgreSQL stores explain their extra.""" import subprocess import sys @@ -6,7 +6,15 @@ import pytest -@pytest.mark.parametrize("module", ["adcp.reporting.production", "adcp.reporting.projection"]) +@pytest.mark.parametrize( + "module", + [ + "adcp.reporting.production", + "adcp.reporting.projection", + "adcp_reporting_storage", + "adcp_reporting_storage.production", + ], +) @pytest.mark.parametrize("drivers", [False, True]) def test_public_exports_and_optional_database_drivers(module, drivers): script = """ @@ -19,21 +27,25 @@ def find_spec(self, fullname, path=None, target=None): raise ModuleNotFoundError(fullname) sys.meta_path.insert(0, NoDrivers()) module = importlib.import_module(sys.argv[1]) -assert len(module.__all__) == len(set(module.__all__)) +exports = getattr(module, '__all__', []) +assert len(exports) == len(set(exports)) assert module.__name__ + '.pg' not in sys.modules -for name in module.__all__: +for name in exports: assert getattr(module, name) is not None, name if not drivers: assert 'psycopg' not in sys.modules assert 'psycopg_pool' not in sys.modules - name = ('PgReportingProductionStore' if module.__name__.endswith('production') - else 'PgReportingProjectionStore') - try: - getattr(module, name)(pool=object()) - except ImportError as error: - assert 'pg' in str(error) - else: - raise AssertionError('PG store constructed without its driver') + # The stores moved to adcp-reporting-storage; adcp no longer binds them. + for name in ('PgReportingProductionStore', 'PgReportingProjectionStore'): + assert not hasattr(module, name) or module.__name__.startswith('adcp_reporting_storage') + if module.__name__ == 'adcp_reporting_storage': + try: + module.PgReportingProductionStore(pool=object()) + except ImportError as error: + assert 'adcp-reporting-storage[pg]' in str(error), str(error) + else: + raise AssertionError('PG store constructed without its driver') + assert 'psycopg' not in sys.modules try: getattr(module, 'NoSuchReportingExport') except AttributeError: diff --git a/tests/test_reporting_provisional_observations.py b/tests/test_reporting_provisional_observations.py index a62d284b6..bd13fc29d 100644 --- a/tests/test_reporting_provisional_observations.py +++ b/tests/test_reporting_provisional_observations.py @@ -8,12 +8,12 @@ from datetime import timedelta, timezone import pytest +from adcp_reporting_storage import PgReportingLedgerStore from pydantic import ValidationError from adcp.reporting.fixtures import OFFICIAL_OFFERING_ID from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, - PgReportingLedgerStore, ReportingProducer, revision_content_sha256, ) diff --git a/tests/test_reporting_row_storage_azure.py b/tests/test_reporting_row_storage_azure.py index 239e09989..c42c2b5af 100644 --- a/tests/test_reporting_row_storage_azure.py +++ b/tests/test_reporting_row_storage_azure.py @@ -11,11 +11,10 @@ core_exceptions = pytest.importorskip("azure.core.exceptions") pytest.importorskip("azure.storage.blob") -from adcp.reporting.ledger.row_storage_azure import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_azure import ( # noqa: E402 create_azure_blob_reporting_row_object_provider_v1, ) -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 -from adcp.reporting.ledger.row_storage_object import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_object import ( # noqa: E402 ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, @@ -23,6 +22,8 @@ run_reporting_row_object_provider_conformance_v1, ) +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 + CONTEXT = ReportingRowObjectContextV1() LOCATION = {"account": "rowsaccount", "container": "rows"} diff --git a/tests/test_reporting_row_storage_filesystem.py b/tests/test_reporting_row_storage_filesystem.py index dc0275a36..099f0e082 100644 --- a/tests/test_reporting_row_storage_filesystem.py +++ b/tests/test_reporting_row_storage_filesystem.py @@ -6,9 +6,7 @@ from pathlib import Path import pytest - -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError -from adcp.reporting.ledger.row_storage_object import ( +from adcp_reporting_storage.ledger.row_storage_object import ( REPORTING_ROW_DEFAULT_KEY_TEMPLATE, ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, @@ -22,6 +20,8 @@ run_reporting_row_object_provider_conformance_v1, ) +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError + CONTEXT = ReportingRowObjectContextV1() NAMESPACE = "a" * 64 @@ -226,7 +226,7 @@ def test_unsafe_prefixes_are_refused(prefix: str) -> None: def object_options(tmp_path: Path, **binding: object): - from adcp.reporting.ledger.row_storage import ( + from adcp_reporting_storage.ledger.row_storage import ( ReportingObjectRowBindingV1, ReportingRowStorageOptionsV1, ) @@ -248,7 +248,7 @@ def object_options(tmp_path: Path, **binding: object): def test_a_writable_template_without_revision_key_is_refused_at_configuration( tmp_path: Path, ) -> None: - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 with pytest.raises(ReportingRowStoreError) as refused: ReportingRowStorageV1(object_options(tmp_path, key_template=OLD_TEMPLATE)) @@ -264,7 +264,7 @@ def test_a_writable_template_without_revision_key_is_refused_at_configuration( def test_the_default_and_an_explicit_template_with_revision_key_are_accepted( tmp_path: Path, ) -> None: - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 ReportingRowStorageV1(object_options(tmp_path)) ReportingRowStorageV1(object_options(tmp_path, key_template=NEW_TEMPLATE)) @@ -274,7 +274,7 @@ def test_the_default_and_an_explicit_template_with_revision_key_are_accepted( def test_a_read_only_binding_is_exempt_but_its_template_is_otherwise_validated( tmp_path: Path, ) -> None: - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 ReportingRowStorageV1(object_options(tmp_path, key_template=OLD_TEMPLATE, read_only=True)) for template in ( @@ -287,7 +287,7 @@ def test_a_read_only_binding_is_exempt_but_its_template_is_otherwise_validated( def test_read_only_is_not_part_of_the_binding_identity(tmp_path: Path) -> None: - from adcp.reporting.ledger.row_storage import ( + from adcp_reporting_storage.ledger.row_storage import ( ReportingObjectRowBindingV1, _binding_identity_sha256, ) diff --git a/tests/test_reporting_row_storage_gcs.py b/tests/test_reporting_row_storage_gcs.py index b9dc1644d..59ecb68f4 100644 --- a/tests/test_reporting_row_storage_gcs.py +++ b/tests/test_reporting_row_storage_gcs.py @@ -10,12 +10,11 @@ exceptions = pytest.importorskip("google.api_core.exceptions") -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 -from adcp.reporting.ledger.row_storage_gcs import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_gcs import ( # noqa: E402 assert_gcs_reporting_row_bucket_policy_v1, create_gcs_reporting_row_object_provider_v1, ) -from adcp.reporting.ledger.row_storage_object import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_object import ( # noqa: E402 ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, @@ -23,6 +22,8 @@ run_reporting_row_object_provider_conformance_v1, ) +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 + CONTEXT = ReportingRowObjectContextV1() LOCATION = {"bucket": "rows-bucket"} @@ -182,7 +183,7 @@ async def test_create_only_upload_sends_the_precondition_and_a_declared_checksum provider = create_gcs_reporting_row_object_provider_v1(storage=storage) created = await provider.put_if_absent(put("adcp-rows/ns/a.jsonl.gz"), CONTEXT) assert created.created and created.native_version_ref.isdigit() - (_, options) = next(call for call in storage.calls if call[0] == "upload") + _, options = next(call for call in storage.calls if call[0] == "upload") assert options["if_generation_match"] == 0 assert options["content_type"] == "application/gzip" and options["checksum"] == "md5" stored = storage.objects[("rows-bucket", "adcp-rows/ns/a.jsonl.gz")][0] @@ -217,7 +218,7 @@ def read(**kwargs: Any) -> Any: ) assert await read(max_bytes=6, byte_range=(4, 6)) == b"456789" - (_, options) = [call for call in storage.calls if call[0] == "download"][-1] + _, options = [call for call in storage.calls if call[0] == "download"][-1] assert options["generation"] == int(created.native_version_ref) assert options["raw_download"] is True and options["checksum"] is None assert (options["start"], options["end"]) == (4, 9) @@ -368,8 +369,8 @@ def test_locations_are_closed_and_options_validated() -> None: async def test_provider_errors_never_leak_their_messages() -> None: - from adcp.reporting.ledger import ReportingObjectRowBindingV1, ReportingRowStorageOptionsV1 - from adcp.reporting.ledger.row_storage import ReportingRowStorageV1 + from adcp_reporting_storage import ReportingObjectRowBindingV1, ReportingRowStorageOptionsV1 + from adcp_reporting_storage.ledger.row_storage import ReportingRowStorageV1 class Broken(FakeStorage): def bucket(self, name: str) -> Any: diff --git a/tests/test_reporting_row_storage_s3.py b/tests/test_reporting_row_storage_s3.py index dc955e663..0a0ff48ab 100644 --- a/tests/test_reporting_row_storage_s3.py +++ b/tests/test_reporting_row_storage_s3.py @@ -10,18 +10,19 @@ boto3 = pytest.importorskip("boto3") moto = pytest.importorskip("moto") -from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 -from adcp.reporting.ledger.row_storage_object import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_object import ( # noqa: E402 ReportingRowObjectContextV1, ReportingRowObjectDeleteInputV1, ReportingRowObjectGetInputV1, ReportingRowObjectPutInputV1, run_reporting_row_object_provider_conformance_v1, ) -from adcp.reporting.ledger.row_storage_s3 import ( # noqa: E402 +from adcp_reporting_storage.ledger.row_storage_s3 import ( # noqa: E402 create_s3_reporting_row_object_provider_v1, ) +from adcp.reporting.ledger.row_storage_errors import ReportingRowStoreError # noqa: E402 + CONTEXT = ReportingRowObjectContextV1() LOCATION = {"bucket": "rows-bucket"} diff --git a/tests/test_reporting_startup_recovery.py b/tests/test_reporting_startup_recovery.py index 33df42db4..f1b1bd1d3 100644 --- a/tests/test_reporting_startup_recovery.py +++ b/tests/test_reporting_startup_recovery.py @@ -11,6 +11,7 @@ from unittest.mock import AsyncMock import pytest +from adcp_reporting_storage import postgres_reliable_reporting_service from adcp.reporting.fixtures import redacted_capabilities from adcp.reporting.ledger import ReportingProducer, WorkerTurn @@ -191,7 +192,7 @@ async def recovery_service_factory(request: Any) -> AsyncIterator[Any]: async with isolated_reporting_pool(autocommit=request.param == "pg-autocommit") as pool: def create(resolver: Any) -> ReliableReportingService: - service = ReliableReportingService.postgres( + service = postgres_reliable_reporting_service( pool=pool, account_context=resolver, clock=lambda: NOW ) service.sources.register( diff --git a/tests/test_reporting_storage_boundary.py b/tests/test_reporting_storage_boundary.py new file mode 100644 index 000000000..df28cf6db --- /dev/null +++ b/tests/test_reporting_storage_boundary.py @@ -0,0 +1,135 @@ +"""The adcp / adcp-reporting-storage boundary. + +``adcp`` defines the reporting store protocols and the in-memory stores; +``adcp-reporting-storage`` implements them over PostgreSQL and object storage. +The dependency points one way: the storage distribution imports ``adcp``, and +``adcp`` never imports the storage distribution or a database driver from its +reporting package. The storage package may reach a short, fixed list of private +``adcp.reporting`` helpers; that list can only shrink. +""" + +from __future__ import annotations + +import ast +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +ADCP_SRC = ROOT / "src" / "adcp" +STORAGE_SRC = ROOT / "packages" / "adcp-reporting-storage" / "src" / "adcp_reporting_storage" + +#: Private ``adcp`` modules the storage distribution imports. Each is a shared +#: codec or helper that both sides must agree on byte for byte; promote one to a +#: public name before adding another. +ALLOWED_PRIVATE_ADCP_IMPORTS = { + "adcp._deferred_adapters", + "adcp.reporting._source_authorization", + "adcp.reporting.feed._errors", + "adcp.reporting.ledger._delivery_state", + "adcp.reporting.ledger._record_rows", + "adcp.reporting.materializer._errors", + "adcp.reporting.receipts._diagnostics", +} + + +def _imports(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8")) + found: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + found.update(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module and not node.level: + found.add(node.module) + return found + + +def test_adcp_never_imports_the_storage_distribution_or_a_reporting_driver() -> None: + offenders: dict[str, set[str]] = {} + for path in ADCP_SRC.rglob("*.py"): + modules = _imports(path) + bad = {m for m in modules if m.split(".")[0] == "adcp_reporting_storage"} + if path.is_relative_to(ADCP_SRC / "reporting"): + bad |= {m for m in modules if m.split(".")[0] in {"psycopg", "psycopg_pool"}} + if bad: + offenders[str(path.relative_to(ROOT))] = bad + assert not offenders, offenders + + +def test_storage_distribution_imports_adcp_through_public_names_or_the_allowlist() -> None: + reaches: dict[str, set[str]] = {} + for path in STORAGE_SRC.rglob("*.py"): + private = { + m + for m in _imports(path) + if m.split(".")[0] == "adcp" + and any(part.startswith("_") for part in m.split(".")) + and m not in ALLOWED_PRIVATE_ADCP_IMPORTS + } + if private: + reaches[str(path.relative_to(ROOT))] = private + assert not reaches, reaches + + +def test_allowlist_names_only_modules_that_exist_and_are_used() -> None: + used: set[str] = set() + for path in STORAGE_SRC.rglob("*.py"): + used |= {m for m in _imports(path) if m in ALLOWED_PRIVATE_ADCP_IMPORTS} + assert used == ALLOWED_PRIVATE_ADCP_IMPORTS, { + "unused": ALLOWED_PRIVATE_ADCP_IMPORTS - used, + } + for module in ALLOWED_PRIVATE_ADCP_IMPORTS: + assert (ADCP_SRC.parent / module.replace(".", "/")).with_suffix(".py").is_file(), module + + +def test_importing_the_storage_facade_loads_no_store_driver_or_cloud_sdk() -> None: + """The facade is lazy: a store loads on first use, a cloud SDK only with its provider.""" + script = f""" +import sys +sys.path[:0] = [{str(ADCP_SRC.parent)!r}, {str(STORAGE_SRC.parent)!r}] +import adcp_reporting_storage +assert not [m for m in sys.modules if m.startswith("adcp_reporting_storage.")] +assert "psycopg" not in sys.modules and "adcp.reporting" not in sys.modules +from adcp_reporting_storage import PgReportingLedgerStore, ReportingRowStorageOptionsV1 +assert PgReportingLedgerStore.__module__ == "adcp_reporting_storage.ledger.pg" +# psycopg may now be loaded (the store probes for it); the cloud SDKs never are. +for name in ("boto3", "google.cloud.storage", "azure.storage.blob"): + assert name not in sys.modules, name +assert "adcp.reporting.ledger.pg" not in sys.modules +missing = [n for n in adcp_reporting_storage.__all__ if getattr(adcp_reporting_storage, n) is None] +assert not missing, missing +print("ok") +""" + result = subprocess.run( + [sys.executable, "-I", "-c", script], capture_output=True, text=True, timeout=120 + ) + assert result.returncode == 0, result.stdout + result.stderr + assert result.stdout.strip() == "ok" + + +def test_moved_names_are_gone_from_adcp() -> None: + """A clean cut: the old addresses raise, with nothing shimming them.""" + import adcp.reporting.ledger as ledger + import adcp.reporting.outbox as outbox + import adcp.reporting.production as production + import adcp.reporting.service as service + + for module, name in ( + (ledger, "PgReportingLedgerStore"), + (ledger, "PgReportingReconciliationStore"), + (ledger, "ReportingRowStorageOptionsV1"), + (ledger, "ReportingChangesPageV1"), + (outbox, "PgReportingOutbox"), + (outbox, "PgStatusNotificationStore"), + (production, "PgReportingProductionStore"), + ): + assert not hasattr(module, name), name + assert name not in module.__all__, name + assert not hasattr(service.ReliableReportingService, "postgres") + for module in ( + "adcp.reporting.ledger.pg", + "adcp.reporting.ledger.row_storage", + "adcp.reporting.outbox.pg", + "adcp.reporting.migration", + ): + assert not (ADCP_SRC.parent / module.replace(".", "/")).with_suffix(".py").exists(), module diff --git a/tests/test_reporting_typed_control_totals.py b/tests/test_reporting_typed_control_totals.py index bf9fbbc87..792b67f45 100644 --- a/tests/test_reporting_typed_control_totals.py +++ b/tests/test_reporting_typed_control_totals.py @@ -15,10 +15,10 @@ import pytest import rfc8785 +from adcp_reporting_storage import PgReportingLedgerStore from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, - PgReportingLedgerStore, ReportingControlTotalRecord, revision_content_sha256, ) diff --git a/tests/type_checks/reliable_reporting_production_context.py b/tests/type_checks/reliable_reporting_production_context.py index 93db6d0f6..429a84fd2 100644 --- a/tests/type_checks/reliable_reporting_production_context.py +++ b/tests/type_checks/reliable_reporting_production_context.py @@ -1,3 +1,5 @@ +from adcp_reporting_storage import postgres_reliable_reporting_service + """Installed public fixed-profile production bridge typing.""" from typing import Any @@ -33,7 +35,7 @@ def compose_adapters( application: ADCPHandler[Any], ) -> ReliableReportingService: offering: ReportingServiceOffering = options.offerings[0] - service = ReliableReportingService.postgres( + service = postgres_reliable_reporting_service( pool=pool, account_context=context, production=options ) service.sources.register(offering.adapter, adapter) diff --git a/tests/type_checks/reporting_caller_ownership.py b/tests/type_checks/reporting_caller_ownership.py index 4b03b76d9..4e35160d4 100644 --- a/tests/type_checks/reporting_caller_ownership.py +++ b/tests/type_checks/reporting_caller_ownership.py @@ -3,6 +3,16 @@ from datetime import datetime from typing import Any +from adcp_reporting_storage.migration import ( + ReportingOwnershipBackfill, + backfill_legacy_reporting, + legacy_generation_digest, + migrate_legacy_reporting, + replace_empty_legacy_reporting, + replace_empty_legacy_reporting_sync, + replace_empty_owned_reporting, + replace_empty_owned_reporting_sync, +) from typing_extensions import assert_type from adcp.reporting.ledger import ( @@ -13,16 +23,6 @@ ReportingLedgerStore, ReportingStatusCaller, ) -from adcp.reporting.migration import ( - ReportingOwnershipBackfill, - backfill_legacy_reporting, - legacy_generation_digest, - migrate_legacy_reporting, - replace_empty_legacy_reporting, - replace_empty_legacy_reporting_sync, - replace_empty_owned_reporting, - replace_empty_owned_reporting_sync, -) async def ownership( diff --git a/tests/type_checks/reporting_durable_materializer.py b/tests/type_checks/reporting_durable_materializer.py index 7ff9b7f4c..6aafceb2c 100644 --- a/tests/type_checks/reporting_durable_materializer.py +++ b/tests/type_checks/reporting_durable_materializer.py @@ -1,10 +1,10 @@ """The optional durable protocol and public lazy imports remain strict-adopter APIs.""" +from adcp_reporting_storage import PgReportingMaterializerStore from typing_extensions import assert_type from adcp.reporting.materializer import ( InMemoryReportingMaterializerStore, - PgReportingMaterializerStore, ReportingDestinationIO, ReportingDestinationWriter, ReportingMaterializerLease, diff --git a/tests/type_checks/reporting_frozen_feed.py b/tests/type_checks/reporting_frozen_feed.py index 9feff103e..613cdb5d3 100644 --- a/tests/type_checks/reporting_frozen_feed.py +++ b/tests/type_checks/reporting_frozen_feed.py @@ -2,11 +2,11 @@ from typing import Any +from adcp_reporting_storage import PgReportingFeedStore from typing_extensions import assert_type from adcp.reporting.feed import ( InMemoryReportingFeedStore, - PgReportingFeedStore, ReportingFeedError, ReportingFeedSnapshot, ReportingFeedStore, diff --git a/tests/type_checks/reporting_inline_storage.py b/tests/type_checks/reporting_inline_storage.py index 04e0a4856..68f6f0727 100644 --- a/tests/type_checks/reporting_inline_storage.py +++ b/tests/type_checks/reporting_inline_storage.py @@ -1,5 +1,6 @@ """Borrowed pool composition with the existing public source protocols.""" +from adcp_reporting_storage.inline_storage import PgReportingSealStore, PgReportingStagingStore from psycopg_pool import AsyncConnectionPool from adcp.reporting.inline_source import ( @@ -8,7 +9,6 @@ ReportingSealStore, ReportingStagingStore, ) -from adcp.reporting.inline_storage import PgReportingSealStore, PgReportingStagingStore from adcp.reporting.source import ReportingSourceCapabilitiesV1, ReportingSourceExecutor diff --git a/tests/type_checks/reporting_notification_outbox.py b/tests/type_checks/reporting_notification_outbox.py index 7267972ae..c0bc09bf0 100644 --- a/tests/type_checks/reporting_notification_outbox.py +++ b/tests/type_checks/reporting_notification_outbox.py @@ -3,19 +3,18 @@ from collections.abc import Callable from datetime import datetime +from adcp_reporting_storage import PgReportingOutbox, PgReportingReconciliationStore from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, InMemoryReportingReconciliationStore, - PgReportingReconciliationStore, ReportingLedgerStore, ReportingObligationRecord, ) from adcp.reporting.outbox import ( InMemoryReportingOutbox, - PgReportingOutbox, ReportingEnvelopeCipher, ReportingNotificationOutbox, ReportingNotificationSubscription, diff --git a/tests/type_checks/reporting_receipt_ingress.py b/tests/type_checks/reporting_receipt_ingress.py index c4f65756c..f3471a051 100644 --- a/tests/type_checks/reporting_receipt_ingress.py +++ b/tests/type_checks/reporting_receipt_ingress.py @@ -2,13 +2,13 @@ from typing import Any +from adcp_reporting_storage import PgReportingReceiptStore from typing_extensions import assert_type from adcp.reporting.ledger import ReportingDeliveryPrincipal from adcp.reporting.materializer import ReportingMaterializerStore from adcp.reporting.receipts import ( InMemoryReportingReceiptStore, - PgReportingReceiptStore, ReceiptAccountResolver, ReportingReceiptBatchStore, ReportingReceiptBoundary, diff --git a/tests/type_checks/reporting_reconciliation_records.py b/tests/type_checks/reporting_reconciliation_records.py index 577c730a5..7cac7d89e 100644 --- a/tests/type_checks/reporting_reconciliation_records.py +++ b/tests/type_checks/reporting_reconciliation_records.py @@ -7,12 +7,12 @@ from datetime import datetime, timezone from typing import Any +from adcp_reporting_storage import PgReportingReconciliationStore from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, InMemoryReportingReconciliationStore, - PgReportingReconciliationStore, ReportingCanonicalDigest, ReportingConfigurationGenerationKey, ReportingControlTotalRecord, diff --git a/tests/type_checks/reporting_status_notifications.py b/tests/type_checks/reporting_status_notifications.py index b18507fa7..7c373eef8 100644 --- a/tests/type_checks/reporting_status_notifications.py +++ b/tests/type_checks/reporting_status_notifications.py @@ -2,11 +2,15 @@ from datetime import datetime +from adcp_reporting_storage import ( + PgReportingLedgerStore, + PgReportingOutbox, + PgStatusNotificationStore, +) from psycopg_pool import AsyncConnectionPool from adcp.reporting.ledger import ( InMemoryReportingLedgerStore, - PgReportingLedgerStore, ReportingLedgerStore, ReportingStatusSnapshot, StatusProjectionInput, @@ -17,8 +21,6 @@ from adcp.reporting.outbox import ( InMemoryReportingOutbox, InMemoryStatusNotificationStore, - PgReportingOutbox, - PgStatusNotificationStore, ReportingNotificationOutbox, ReportingStatusNotificationLifecycle, ReportingStatusProjector, diff --git a/tests/type_checks/reporting_webhook_activity.py b/tests/type_checks/reporting_webhook_activity.py index d73b7a3d8..8a4a5194f 100644 --- a/tests/type_checks/reporting_webhook_activity.py +++ b/tests/type_checks/reporting_webhook_activity.py @@ -3,12 +3,13 @@ from datetime import datetime from typing import Any +from adcp_reporting_storage import PgReportingOutbox + from adcp.decisioning.accounts import ResolveContext from adcp.reporting.outbox import ( ActivityOutcome, ActivityRequest, InMemoryReportingOutbox, - PgReportingOutbox, ReportingActivityProjector, ReportingActivityStore, WebhookAttempt,