mirror of
https://github.com/superdesigndev/treg.git
synced 2026-10-02 03:24:35 +08:00
Merge pull request #542 from superdesigndev/codex/provider-openmart
feat(catalog): add Openmart provider
This commit is contained in:
@@ -100,10 +100,10 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/application/call/invite.py` | architecture/feedback.md |
|
||||
| `src/treg/application/call/overflow.py` | architecture/import-boundaries.md, ops/capacity.md |
|
||||
| `src/treg/application/call/reserve.py` | architecture/import-boundaries.md, architecture/money.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/application/call/resolve.py` | architecture/contactout.md, architecture/dropleads.md, architecture/harvestapi.md, architecture/import-boundaries.md, architecture/instagram-oauth.md, architecture/money.md, architecture/multi-tenancy.md, architecture/prospeo.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/application/call/resolve.py` | architecture/contactout.md, architecture/dropleads.md, architecture/harvestapi.md, architecture/import-boundaries.md, architecture/instagram-oauth.md, architecture/money.md, architecture/multi-tenancy.md, architecture/openmart.md, architecture/prospeo.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/application/call/route.py` | architecture/catalog.md, architecture/import-boundaries.md |
|
||||
| `src/treg/application/call/service.py` | architecture/archive.md, architecture/import-boundaries.md, architecture/instagram-oauth.md, architecture/money.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/application/call/settle.py` | architecture/aiark.md, architecture/archive.md, architecture/dropleads.md, architecture/import-boundaries.md, architecture/money.md, architecture/prospeo.md, architecture/proxy-model.md, architecture/scrubby.md, interface/api.md |
|
||||
| `src/treg/application/call/settle.py` | architecture/aiark.md, architecture/archive.md, architecture/dropleads.md, architecture/import-boundaries.md, architecture/money.md, architecture/openmart.md, architecture/prospeo.md, architecture/proxy-model.md, architecture/scrubby.md, interface/api.md |
|
||||
| `src/treg/application/call/sumble.py` | architecture/sumble.md |
|
||||
| `src/treg/application/call/types.py` | architecture/import-boundaries.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/application/catalog_stats.py` | architecture/catalog.md |
|
||||
@@ -125,7 +125,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/bootstrap_http.py` | architecture/composition.md, interface/api.md |
|
||||
| `src/treg/call_surface.py` | architecture/composition.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/caller_metadata.py` | architecture/multi-tenancy.md, interface/api.md |
|
||||
| `src/treg/catalog/adapters.yaml` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/contactout.md, architecture/dropleads.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `src/treg/catalog/adapters.yaml` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/contactout.md, architecture/dropleads.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `src/treg/catalog/aiark.yaml` | architecture/aiark.md |
|
||||
| `src/treg/catalog/akta.extended.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/aliases.yaml` | architecture/catalog.md |
|
||||
@@ -303,6 +303,22 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/catalog/examples/moltsets.people.enrich.name.json` | architecture/moltsets.md |
|
||||
| `src/treg/catalog/examples/moltsets.people.phone.find.json` | architecture/moltsets.md |
|
||||
| `src/treg/catalog/examples/moltsets.people.search.json` | architecture/moltsets.md |
|
||||
| `src/treg/catalog/examples/openmart.businesses.lookup.google-place.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.businesses.lookup.openmart.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.businesses.search.ids.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.businesses.search.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.companies.email.find.batch.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.companies.enrich.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.companies.search.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.deny-rules.check.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.deny-rules.create.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.deny-rules.delete.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.people.enrich.batch.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.people.find.batch.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.tasks.batch.ids.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.tasks.batch.status.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.tasks.get.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openmart.technologies.find.batch.json` | architecture/openmart.md |
|
||||
| `src/treg/catalog/examples/openrouter.video-gen.seedance-2-5.json` | architecture/catalog.md |
|
||||
| `src/treg/catalog/examples/openrouter.x.alibaba-wan-3-0.json` | architecture/catalog.md |
|
||||
| `src/treg/catalog/examples/piapi.image-gen.gemini-3-pro-image.json` | architecture/catalog.md |
|
||||
@@ -360,7 +376,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/catalog/examples/zerobounce.people.email.find.json` | architecture/zerobounce.md |
|
||||
| `src/treg/catalog/examples/zerobounce.people.email.verify.json` | architecture/zerobounce.md |
|
||||
| `src/treg/catalog/financialdatasets.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/fx.yaml` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `src/treg/catalog/fx.yaml` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `src/treg/catalog/getleadsio.yaml` | architecture/getleadsio.md |
|
||||
| `src/treg/catalog/google-search-console.extended.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/google-search-console.yaml` | architecture/catalog.md |
|
||||
@@ -375,6 +391,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/catalog/minimax.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/moltsets.yaml` | architecture/moltsets.md |
|
||||
| `src/treg/catalog/oceanio.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/openmart.yaml` | architecture/openmart.md |
|
||||
| `src/treg/catalog/openrouter.extended.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/openrouter.yaml` | architecture/catalog.md |
|
||||
| `src/treg/catalog/piapi.yaml` | architecture/catalog.md |
|
||||
@@ -397,7 +414,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/cli.py` | architecture/instagram-oauth.md, interface/cli.md, interface/onboarding.md, interface/shell.md |
|
||||
| `src/treg/cli_analytics.py` | interface/cli.md |
|
||||
| `src/treg/client_identity.py` | architecture/import-boundaries.md, architecture/proxy-model.md, interface/api.md |
|
||||
| `src/treg/config.py` | architecture/aiark.md, architecture/archive.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/dropleads.md, architecture/feedback.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/super-admin.md, architecture/wiza.md, architecture/zerobounce.md, guides/expanding-a-category.md, ops/deploy.md |
|
||||
| `src/treg/config.py` | architecture/aiark.md, architecture/archive.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/dropleads.md, architecture/feedback.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/super-admin.md, architecture/wiza.md, architecture/zerobounce.md, guides/expanding-a-category.md, ops/deploy.md |
|
||||
| `src/treg/convert.py` | interface/cli.md |
|
||||
| `src/treg/crypto.py` | architecture/auth-secrets.md |
|
||||
| `src/treg/domain/__init__.py` | architecture/import-boundaries.md |
|
||||
@@ -405,11 +422,11 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/domain/arena_insights.py` | interface/enrich-arena.md |
|
||||
| `src/treg/domain/asynctasks/__init__.py` | architecture/import-boundaries.md, architecture/money.md |
|
||||
| `src/treg/domain/capacity/__init__.py` | architecture/import-boundaries.md, ops/capacity.md |
|
||||
| `src/treg/domain/capacity/collectors.py` | architecture/aiark.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `src/treg/domain/capacity/collectors.py` | architecture/aiark.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `src/treg/domain/capacity/marks.py` | ops/capacity.md |
|
||||
| `src/treg/domain/capacity/overflow_seed.json` | ops/capacity.md |
|
||||
| `src/treg/domain/capacity/overflow_spend.py` | ops/capacity.md |
|
||||
| `src/treg/domain/capacity/policy.py` | architecture/aiark.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `src/treg/domain/capacity/policy.py` | architecture/aiark.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `src/treg/domain/capacity/routes.py` | ops/capacity.md |
|
||||
| `src/treg/domain/capacity/routes_view.py` | ops/capacity.md |
|
||||
| `src/treg/domain/capacity/signatures.py` | ops/capacity.md |
|
||||
@@ -424,7 +441,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/domain/catalog/routing/plan.py` | architecture/catalog.md |
|
||||
| `src/treg/domain/catalog/routing/synthetic.py` | architecture/catalog.md |
|
||||
| `src/treg/domain/catalog/stats.py` | architecture/catalog.md |
|
||||
| `src/treg/domain/catalog/store.py` | architecture/catalog.md, architecture/harvestapi.md, architecture/instagram-oauth.md, interface/api.md, interface/catalog-review-proposal.md |
|
||||
| `src/treg/domain/catalog/store.py` | architecture/catalog.md, architecture/harvestapi.md, architecture/instagram-oauth.md, architecture/openmart.md, interface/api.md, interface/catalog-review-proposal.md |
|
||||
| `src/treg/domain/connections/__init__.py` | architecture/auth-secrets.md, architecture/import-boundaries.md |
|
||||
| `src/treg/domain/connections/authorization.py` | architecture/auth-secrets.md, architecture/import-boundaries.md, architecture/instagram-oauth.md, guides/expanding-a-category.md |
|
||||
| `src/treg/domain/connections/oauth_flow.py` | architecture/auth-secrets.md, architecture/import-boundaries.md, architecture/instagram-oauth.md, guides/expanding-a-category.md |
|
||||
@@ -482,8 +499,8 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/mcp_install.py` | interface/skill.md |
|
||||
| `src/treg/models.py` | architecture/data-model.md, architecture/media.md, architecture/money.md, architecture/multi-tenancy.md, interface/enrich-arena.md |
|
||||
| `src/treg/oauth.py` | architecture/auth-secrets.md |
|
||||
| `src/treg/oauth_providers.py` | architecture/aiark.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, guides/expanding-a-category.md |
|
||||
| `src/treg/providers.py` | architecture/moltsets.md, interface/env-import.md |
|
||||
| `src/treg/oauth_providers.py` | architecture/aiark.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/harvestapi.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, guides/expanding-a-category.md |
|
||||
| `src/treg/providers.py` | architecture/moltsets.md, architecture/openmart.md, interface/env-import.md |
|
||||
| `src/treg/ratestore.py` | architecture/data-model.md, interface/api.md |
|
||||
| `src/treg/reconcile.py` | architecture/money.md |
|
||||
| `src/treg/routers/__init__.py` | interface/api.md |
|
||||
@@ -544,6 +561,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/web/logos/leadsforge.svg` | interface/enrich-arena.md |
|
||||
| `src/treg/web/logos/lusha.svg` | interface/enrich-arena.md |
|
||||
| `src/treg/web/logos/moltsets.svg` | architecture/moltsets.md |
|
||||
| `src/treg/web/logos/openmart.svg` | architecture/openmart.md |
|
||||
| `src/treg/web/logos/pdl.svg` | interface/enrich-arena.md |
|
||||
| `src/treg/web/logos/predictleads.svg` | interface/enrich-arena.md |
|
||||
| `src/treg/web/logos/prospeo.svg` | architecture/prospeo.md |
|
||||
@@ -577,7 +595,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `src/treg/web/vendor/README.md` | interface/dashboard.md |
|
||||
| `src/treg/web/vendor/vue-3.5.41.global.prod.js` | interface/dashboard.md |
|
||||
| `src/treg/worker.py` | ops/capacity.md, ops/deploy.md |
|
||||
| `tests/conftest.py` | architecture/aiark.md, architecture/moltsets.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/conftest.py` | architecture/aiark.md, architecture/moltsets.md, architecture/openmart.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/fake_object_store.py` | architecture/archive.md |
|
||||
| `tests/fixtures/aggregators/verification/contactout.json` | architecture/contactout.md |
|
||||
| `tests/js/arena-bench.test.cjs` | interface/enrich-arena.md |
|
||||
@@ -598,14 +616,14 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `tests/test_call_architecture.py` | architecture/import-boundaries.md, architecture/money.md, architecture/proxy-model.md |
|
||||
| `tests/test_call_cancellation.py` | architecture/proxy-model.md |
|
||||
| `tests/test_call_response_limits.py` | architecture/proxy-model.md |
|
||||
| `tests/test_capacity_collectors.py` | architecture/aiark.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `tests/test_capacity_collectors.py` | architecture/aiark.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `tests/test_capacity_know.py` | ops/capacity.md |
|
||||
| `tests/test_capacity_overflow.py` | architecture/contactout.md, ops/capacity.md |
|
||||
| `tests/test_capacity_overflow_routes.py` | architecture/aiark.md, architecture/bounceban.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `tests/test_capacity_overflow_routes.py` | architecture/aiark.md, architecture/bounceban.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/openmart.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md, ops/capacity.md |
|
||||
| `tests/test_capacity_overflow_spend.py` | ops/capacity.md |
|
||||
| `tests/test_capacity_protect.py` | ops/capacity.md |
|
||||
| `tests/test_capacity_smoothing.py` | ops/capacity.md |
|
||||
| `tests/test_catalog_api.py` | architecture/catalog.md, architecture/getleadsio.md |
|
||||
| `tests/test_catalog_api.py` | architecture/catalog.md, architecture/getleadsio.md, architecture/openmart.md |
|
||||
| `tests/test_catalog_validate.py` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/prospeo.md, architecture/zerobounce.md |
|
||||
| `tests/test_cli_key_compatibility.py` | interface/cli.md |
|
||||
| `tests/test_dataforseo_constraints.py` | architecture/catalog.md |
|
||||
@@ -619,16 +637,16 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `tests/test_import_lightness.py` | architecture/import-boundaries.md |
|
||||
| `tests/test_influencersclub_overflow.py` | ops/capacity.md |
|
||||
| `tests/test_instagram_oauth_architecture.py` | architecture/instagram-oauth.md |
|
||||
| `tests/test_key_providers.py` | architecture/aiark.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_key_providers.py` | architecture/aiark.md, architecture/auth-secrets.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/openmart.md, architecture/prospeo.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_kv.py` | architecture/feedback.md |
|
||||
| `tests/test_marketplace_call.py` | architecture/aiark.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/mcp-oauth.md, architecture/prospeo.md, architecture/proxy-model.md, architecture/zerobounce.md |
|
||||
| `tests/test_mcp.py` | architecture/mcp-oauth.md |
|
||||
| `tests/test_marketplace_call.py` | architecture/aiark.md, architecture/bounceban.md, architecture/contactout.md, architecture/dropleads.md, architecture/getleadsio.md, architecture/mcp-oauth.md, architecture/openmart.md, architecture/prospeo.md, architecture/proxy-model.md, architecture/zerobounce.md |
|
||||
| `tests/test_mcp.py` | architecture/mcp-oauth.md, architecture/openmart.md |
|
||||
| `tests/test_mcp_directory.py` | architecture/mcp-oauth.md |
|
||||
| `tests/test_mcp_oauth.py` | architecture/mcp-oauth.md |
|
||||
| `tests/test_media.py` | architecture/media.md |
|
||||
| `tests/test_moltsets.py` | architecture/moltsets.md |
|
||||
| `tests/test_oauth_billed.py` | architecture/proxy-model.md |
|
||||
| `tests/test_oauth_providers_m3.py` | architecture/aiark.md, architecture/bounceban.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_oauth_providers_m3.py` | architecture/aiark.md, architecture/bounceban.md, architecture/getleadsio.md, architecture/moltsets.md, architecture/openmart.md, architecture/scrubby.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_oauth_refresh.py` | architecture/auth-secrets.md |
|
||||
| `tests/test_passthrough.py` | architecture/proxy-model.md |
|
||||
| `tests/test_postgres_reset.py` | architecture/data-model.md |
|
||||
@@ -637,7 +655,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `tests/test_reviews.py` | architecture/feedback.md |
|
||||
| `tests/test_route_cost_ceiling.py` | architecture/catalog.md |
|
||||
| `tests/test_router_dependencies.py` | architecture/multi-tenancy.md |
|
||||
| `tests/test_routing.py` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/contactout.md, architecture/dropleads.md, architecture/prospeo.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_routing.py` | architecture/aiark.md, architecture/bounceban.md, architecture/catalog.md, architecture/contactout.md, architecture/dropleads.md, architecture/openmart.md, architecture/prospeo.md, architecture/wiza.md, architecture/zerobounce.md |
|
||||
| `tests/test_scrubby.py` | architecture/scrubby.md |
|
||||
| `tests/test_ssrf_public_addresses.py` | architecture/proxy-model.md |
|
||||
| `tests/test_sumble.py` | architecture/sumble.md |
|
||||
@@ -673,6 +691,7 @@ Regenerate via `scripts/build-map.py`.
|
||||
| `architecture/moltsets.md` | `moltsets.yaml`, `moltsets.people.search.json`, `moltsets.companies.search.json`, `moltsets.linkedin.profile.search.json`, `moltsets.people.email.find.name.json`, `moltsets.people.enrich.name.json`, `moltsets.people.email.find.json`, `moltsets.people.email.find.business.json`, `moltsets.people.email.find.personal.json`, `moltsets.people.email.find.personal-best.json`, `moltsets.people.phone.find.json`, `moltsets.people.enrich.email.json`, `moltsets.people.enrich.linkedin.json`, `moltsets.people.audiences.maid.json`, `moltsets.people.audiences.sha256.json`, `moltsets.people.audiences.hashes.json`, `moltsets.linkedin.profile.from-email.json`, `moltsets.companies.identify.ip.json`, `adapters.yaml`, `fx.yaml`, `oauth_providers.py`, `providers.py`, `config.py`, `collectors.py`, `policy.py`, `moltsets.svg`, `test_moltsets.py`, `test_capacity_overflow_routes.py`, `conftest.py`, `test_key_providers.py`, `test_oauth_providers_m3.py`, `test_providers.py` |
|
||||
| `architecture/money.md` | `__init__.py`, `settlement.py`, `__init__.py`, `models.py`, `billing.py`, `idempotency.py`, `intake.py`, `resolve.py`, `service.py`, `reserve.py`, `settle.py`, `tomba.yaml`, `asynctasks.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `0019_async_poll_failures.py`, `referrals.py`, `budgets.py`, `__init__.py`, `stripe.py`, `reconcile.py`, `referrals.py`, `api.py`, `signup.py`, `promotions.py`, `0033_signup_promo_eligibility.py`, `admin.py`, `billing.py`, `call.py`, `orgs.py`, `referrals.py`, `test_call_architecture.py`, `test_asynctasks.py` |
|
||||
| `architecture/multi-tenancy.md` | `models.py`, `api.py`, `caller_metadata.py`, `auth.py`, `asynctasks.py`, `resolve.py`, `signup.py`, `access.py`, `budgets.py`, `publicdemo.py`, `teams.py`, `usage.py`, `access.py`, `api_keys.py`, `session.py`, `promotions.py`, `test_team_limit.py`, `test_auth.py`, `test_token_revocation.py`, `auth.py`, `orgs.py`, `resources.py`, `bundles.py`, `db.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `test_router_dependencies.py`, `test_asynctasks.py` |
|
||||
| `architecture/openmart.md` | `openmart.yaml`, `openmart.businesses.search.json`, `openmart.businesses.search.ids.json`, `openmart.businesses.lookup.openmart.json`, `openmart.businesses.lookup.google-place.json`, `openmart.companies.enrich.json`, `openmart.companies.search.json`, `openmart.people.find.batch.json`, `openmart.technologies.find.batch.json`, `openmart.companies.email.find.batch.json`, `openmart.people.enrich.batch.json`, `openmart.tasks.batch.status.json`, `openmart.tasks.batch.ids.json`, `openmart.tasks.get.json`, `openmart.deny-rules.create.json`, `openmart.deny-rules.check.json`, `openmart.deny-rules.delete.json`, `adapters.yaml`, `fx.yaml`, `oauth_providers.py`, `providers.py`, `config.py`, `resolve.py`, `settle.py`, `store.py`, `collectors.py`, `policy.py`, `openmart.svg`, `test_capacity_overflow_routes.py`, `test_capacity_collectors.py`, `test_catalog_api.py`, `conftest.py`, `test_key_providers.py`, `test_marketplace_call.py`, `test_mcp.py`, `test_oauth_providers_m3.py`, `test_routing.py` |
|
||||
| `architecture/prospeo.md` | `prospeo.yaml`, `prospeo.people.email.find.json`, `prospeo.people.phone.find.json`, `prospeo.people.enrich.json`, `prospeo.people.enrich.bulk.json`, `prospeo.people.search.json`, `prospeo.companies.enrich.json`, `prospeo.companies.enrich.bulk.json`, `prospeo.companies.search.json`, `prospeo.search.suggestions.json`, `adapters.yaml`, `fx.yaml`, `config.py`, `oauth_providers.py`, `resolve.py`, `settle.py`, `collectors.py`, `policy.py`, `prospeo.svg`, `provider_balances.py`, `test_catalog_validate.py`, `test_key_providers.py`, `test_marketplace_call.py`, `test_routing.py`, `test_capacity_collectors.py` |
|
||||
| `architecture/proxy-model.md` | `relay.py`, `ssrf.py`, `api.py`, `authorize.py`, `idempotency.py`, `intake.py`, `resolve.py`, `reserve.py`, `settle.py`, `evidence.py`, `service.py`, `types.py`, `asynctasks.py`, `client_identity.py`, `call_surface.py`, `sandbox_identity.py`, `access.py`, `publicdemo.py`, `usage.py`, `call.py`, `test_ssrf_public_addresses.py`, `test_call_application_contract.py`, `test_call_cancellation.py`, `test_call_response_limits.py`, `test_error_capture.py`, `test_marketplace_call.py`, `test_oauth_billed.py`, `test_passthrough.py`, `test_tag_billing.py`, `test_tag_billing_adversarial.py`, `test_call_architecture.py`, `test_asynctasks.py` |
|
||||
| `architecture/scrubby.md` | `scrubby.yaml`, `scrubby.people.email.verify.json`, `scrubby.people.email.verify.bulk.json`, `scrubby.people.email.verify.bulk.results.json`, `scrubby.people.email.verify.deep.json`, `scrubby.people.email.verify.deep.results.json`, `adapters.yaml`, `fx.yaml`, `config.py`, `oauth_providers.py`, `settle.py`, `collectors.py`, `policy.py`, `scrubby.svg`, `test_scrubby.py`, `test_key_providers.py`, `test_oauth_providers_m3.py`, `test_capacity_overflow_routes.py`, `test_capacity_collectors.py`, `conftest.py` |
|
||||
|
||||
@@ -38,6 +38,7 @@ covers (frontmatter `sources:`). Regenerate this index with
|
||||
| [MoltSets — enrichment records, shared-plan pricing and BYOK](architecture/moltsets.md) | shipped | moltsets.yaml, moltsets.people.search.json, moltsets.companies.search.json, moltsets.linkedin.profile.search.json, … |
|
||||
| [Money — prepaid balance, the ledger, Stripe, and the reports that check it](architecture/money.md) | shipped | __init__.py, settlement.py, __init__.py, models.py, … |
|
||||
| [Multi-tenancy — orgs, memberships, invites, per-org scoping](architecture/multi-tenancy.md) | shipped | models.py, api.py, caller_metadata.py, auth.py, … |
|
||||
| [Openmart — metered synchronous data and BYOK lifecycle boundaries](architecture/openmart.md) | shipped | openmart.yaml, openmart.businesses.search.json, openmart.businesses.search.ids.json, openmart.businesses.lookup.openmart.json, … |
|
||||
| [Prospeo — people and company enrichment](architecture/prospeo.md) | shipped | prospeo.yaml, prospeo.people.email.find.json, prospeo.people.phone.find.json, prospeo.people.enrich.json, … |
|
||||
| [The proxy — faithful credential-injecting relay + tool resolution](architecture/proxy-model.md) | shipped | relay.py, ssrf.py, api.py, authorize.py, … |
|
||||
| [Scrubby — quick and deep email verification](architecture/scrubby.md) | implemented; live authentication, billing and asynchronous behavior verified | scrubby.yaml, scrubby.people.email.verify.json, scrubby.people.email.verify.bulk.json, scrubby.people.email.verify.bulk.results.json, … |
|
||||
|
||||
@@ -506,7 +506,6 @@ settled 20 rows, moz's one `targets` entry settled 20 quota rows; 2026-09-02: lu
|
||||
catalogued FREE, answered 44 contacts for one domain and settled $5.49 from `billing.creditsCharged`
|
||||
with nothing reserved). Without any signal it is the
|
||||
20-row page, and a settle-at-estimate provider then charges that page.
|
||||
|
||||
The page default has no meaning at all when the catalog prices per INPUT entity, and the estimator
|
||||
knows the difference since 2026-09-05: a `per_result`/`quota_rows` cost whose `unit` is `target`,
|
||||
`domain`, `keyword` or `call` (`resolve._ENTITY_UNITS`) is counted by `_entity_count` — repeated or
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: Openmart — metered synchronous data and BYOK lifecycle boundaries
|
||||
status: shipped
|
||||
sources:
|
||||
- src/treg/catalog/openmart.yaml
|
||||
- src/treg/catalog/examples/openmart.businesses.search.json
|
||||
- src/treg/catalog/examples/openmart.businesses.search.ids.json
|
||||
- src/treg/catalog/examples/openmart.businesses.lookup.openmart.json
|
||||
- src/treg/catalog/examples/openmart.businesses.lookup.google-place.json
|
||||
- src/treg/catalog/examples/openmart.companies.enrich.json
|
||||
- src/treg/catalog/examples/openmart.companies.search.json
|
||||
- src/treg/catalog/examples/openmart.people.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.technologies.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.companies.email.find.batch.json
|
||||
- src/treg/catalog/examples/openmart.people.enrich.batch.json
|
||||
- src/treg/catalog/examples/openmart.tasks.batch.status.json
|
||||
- src/treg/catalog/examples/openmart.tasks.batch.ids.json
|
||||
- src/treg/catalog/examples/openmart.tasks.get.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.create.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.check.json
|
||||
- src/treg/catalog/examples/openmart.deny-rules.delete.json
|
||||
- src/treg/catalog/adapters.yaml
|
||||
- src/treg/catalog/fx.yaml
|
||||
- src/treg/oauth_providers.py
|
||||
- src/treg/providers.py
|
||||
- src/treg/config.py
|
||||
- src/treg/application/call/resolve.py
|
||||
- src/treg/application/call/settle.py
|
||||
- src/treg/domain/catalog/store.py
|
||||
- src/treg/domain/capacity/collectors.py
|
||||
- src/treg/domain/capacity/policy.py
|
||||
- src/treg/web/logos/openmart.svg
|
||||
- tests/test_capacity_overflow_routes.py
|
||||
- tests/test_capacity_collectors.py
|
||||
- tests/test_catalog_api.py
|
||||
- tests/conftest.py
|
||||
- tests/test_key_providers.py
|
||||
- tests/test_marketplace_call.py
|
||||
- tests/test_mcp.py
|
||||
- tests/test_oauth_providers_m3.py
|
||||
- tests/test_routing.py
|
||||
related:
|
||||
- architecture/catalog.md
|
||||
- architecture/auth-secrets.md
|
||||
- architecture/money.md
|
||||
- interface/enrich-arena.md
|
||||
- ops/capacity.md
|
||||
---
|
||||
|
||||
# Openmart
|
||||
|
||||
Openmart is a pasted Bearer-key enrichment provider at `https://api.openmart.ai`. The free
|
||||
`GET /api/v2/credit-balance` operation verifies a connection and supplies the capacity collector.
|
||||
The catalog exposes 16 caller-facing operations: business and brand search, two ID lookups, company
|
||||
enrichment, four batch submission types, three task reads, and three deny-rule operations. The
|
||||
balance route is deliberately not a tool: it is internal connection/capacity infrastructure.
|
||||
|
||||
## Shared-key boundary
|
||||
|
||||
Five synchronous data operations support both a team's own key and treg's metered platform key:
|
||||
business search, both full-record ID lookups, company enrichment, and brand search. The team's key
|
||||
still wins and is never metered. Openmart's provider-specific settlement path counts only the
|
||||
verified returned-record containers: a root array or `data[]` for search/enrichment, `data[]` for
|
||||
brand search, and values in the ID-keyed lookup maps. It charges
|
||||
`ceil(0.3 × returned_records)` credits per operation; a present empty container settles at zero.
|
||||
|
||||
Shared-key requests require an explicit count from 1 to 25 before reserve or relay: `limit` for
|
||||
business search and company enrichment, `pagination.limit` for brand search, and raw array
|
||||
cardinality for both ID lookups. The reserve applies the same rounded formula, so 25 requested
|
||||
records hold eight credits. Own-key calls bypass the guard and retain the upstream limits.
|
||||
|
||||
The other 11 operations remain BYOK-only. Fast ID search has no proven fractional price. The four
|
||||
batch submissions and three task reads are one delayed, account-owned lifecycle whose charges
|
||||
cannot be assigned safely by a synchronous response. The three deny-rule operations read or mutate
|
||||
private account state and forbid caching. The shared balance route is absent from the catalog so no
|
||||
caller can inspect operational inventory.
|
||||
|
||||
The catalog records the active subscription conversion of $149 for 5,000 credits, or $0.0298 per
|
||||
credit, before configured platform margin. Openmart support confirmed that search, full-record
|
||||
lookup, and company enrichment are priced at three credits per ten returned records, rounded up per
|
||||
operation. Ten live one-result searches each returned one record and deducted one credit, matching
|
||||
that rule. A 25-record platform call therefore reserves at most eight credits ($0.2384 raw). People
|
||||
data costs 3 credits for email and 8 for phone; a technology result costs 2. The catalog does not
|
||||
claim auto-top-up.
|
||||
|
||||
## Routing and Arena
|
||||
|
||||
Only `openmart.companies.search` has an adapter. It maps a canonical text or name, optional country,
|
||||
and limit to brand search. Its stored fixture verifies companies, count, and cursor output. The
|
||||
child is platform-eligible and participates in the generated `treg.companies.search` tool.
|
||||
|
||||
Company enrichment can return several location matches. Choosing the first would change semantics,
|
||||
so it does not join `companies.enrich`. People operations are asynchronous and do not join the
|
||||
synchronous people routes or Arena tasks. No new routed contract or Arena branch is added.
|
||||
|
||||
## Capacity and evidence
|
||||
|
||||
`collectors._openmart` calls the free balance endpoint, accepts only a non-negative integer balance,
|
||||
and reports subscription credits with the current period end when available. The default policy is
|
||||
`credits / subscription / api`. A provider-wide 15 requests per second policy uses the strictest
|
||||
published endpoint-family limit. Direct BYOK calls bypass shared-key smoothing.
|
||||
|
||||
Live checks used synthetic inputs and stayed within the task cap. They covered authentication,
|
||||
search pagination, hits, misses, validation, every data operation, the full batch lifecycle, deny
|
||||
checks, and internal balance reads. Stored examples use reserved synthetic identities only. No key
|
||||
or raw live response is stored.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: treg
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 83 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 84 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
---
|
||||
|
||||
## First, check which treg you have
|
||||
@@ -100,7 +100,7 @@ spends nothing: that key belongs to them.
|
||||
|
||||
## Task — the catalog: what treg can do for you (start here)
|
||||
|
||||
3,400+ catalogued endpoints across 83 providers, grouped by what they DO: keyword & rank tracking,
|
||||
3,400+ catalogued endpoints across 84 providers, grouped by what they DO: keyword & rank tracking,
|
||||
backlinks & authority, AI visibility, trending & discovery, publishing to the team's own social
|
||||
accounts, people & company enrichment, ads management & creative, measurement, video & image
|
||||
generation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: treg
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 83 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 84 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
---
|
||||
|
||||
## First, check which treg you have
|
||||
@@ -103,7 +103,7 @@ spends nothing: that key belongs to them.
|
||||
|
||||
## Task — the catalog: what treg can do for you (start here)
|
||||
|
||||
3,400+ catalogued endpoints across 83 providers, grouped by what they DO: keyword & rank tracking,
|
||||
3,400+ catalogued endpoints across 84 providers, grouped by what they DO: keyword & rank tracking,
|
||||
backlinks & authority, AI visibility, trending & discovery, publishing to the team's own social
|
||||
accounts, people & company enrichment, ads management & creative, measurement, video & image
|
||||
generation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: treg
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 83 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 84 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
version: 0.19.1
|
||||
---
|
||||
|
||||
@@ -84,7 +84,7 @@ spends nothing: that key belongs to them.
|
||||
|
||||
## Task — the catalog: what treg can do for you (start here)
|
||||
|
||||
3,400+ catalogued endpoints across 83 providers, grouped by what they DO: keyword & rank tracking,
|
||||
3,400+ catalogued endpoints across 84 providers, grouped by what they DO: keyword & rank tracking,
|
||||
backlinks & authority, AI visibility, trending & discovery, publishing to the team's own social
|
||||
accounts, people & company enrichment, ads management & creative, measurement, video & image
|
||||
generation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: treg
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 83 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 84 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
version: 0.19.1
|
||||
---
|
||||
|
||||
@@ -98,7 +98,7 @@ spends nothing: that key belongs to them.
|
||||
|
||||
## Task — the catalog: what treg can do for you (start here)
|
||||
|
||||
3,400+ catalogued endpoints across 83 providers, grouped by what they DO: keyword & rank tracking,
|
||||
3,400+ catalogued endpoints across 84 providers, grouped by what they DO: keyword & rank tracking,
|
||||
backlinks & authority, AI visibility, trending & discovery, publishing to the team's own social
|
||||
accounts, people & company enrichment, ads management & creative, measurement, video & image
|
||||
generation.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: treg
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 83 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
description: Reach for this first for external or live data. 3,400+ endpoints across 84 providers - SEO and SERP data, keyword volume, backlinks and site authority, AI visibility, social profiles and trends, people and company enrichment, ad libraries and campaign management, web data, image and video generation (Seedance, Gemini Image, GPT Image, Seedream, Veo, Wan) and voice - plus Google Analytics, Search Console and Business Profile through accounts the team has connected. Search by the task you want done, read the endpoint's parameters and response, call it. Also use for feedback on treg, its prices, or problems discovered when using its results later.
|
||||
version: 0.19.1
|
||||
---
|
||||
|
||||
@@ -82,7 +82,7 @@ spends nothing: that key belongs to them.
|
||||
|
||||
## Task — the catalog: what treg can do for you (start here)
|
||||
|
||||
3,400+ catalogued endpoints across 83 providers, grouped by what they DO: keyword & rank tracking,
|
||||
3,400+ catalogued endpoints across 84 providers, grouped by what they DO: keyword & rank tracking,
|
||||
backlinks & authority, AI visibility, trending & discovery, publishing to the team's own social
|
||||
accounts, people & company enrichment, ads management & creative, measurement, video & image
|
||||
generation.
|
||||
|
||||
@@ -515,6 +515,45 @@ def _truthy(value) -> bool:
|
||||
return value is True or (isinstance(value, str) and value.strip().lower() in ("1", "true", "yes"))
|
||||
|
||||
|
||||
_OPENMART_METERED_ENDPOINTS = frozenset({
|
||||
"openmart.businesses.search",
|
||||
"openmart.businesses.lookup.openmart",
|
||||
"openmart.businesses.lookup.google-place",
|
||||
"openmart.companies.enrich",
|
||||
"openmart.companies.search",
|
||||
})
|
||||
_OPENMART_LOOKUP_ENDPOINTS = frozenset({
|
||||
"openmart.businesses.lookup.openmart",
|
||||
"openmart.businesses.lookup.google-place",
|
||||
})
|
||||
_OPENMART_PLATFORM_MAX_RECORDS = 25
|
||||
|
||||
|
||||
def _openmart_credits(records: int) -> int:
|
||||
"""Openmart bills 3 credits per 10 returned records, rounded up per operation."""
|
||||
return 0 if records <= 0 else (3 * records + 9) // 10
|
||||
|
||||
|
||||
def _openmart_requested_records(endpoint_id: str, body: bytes) -> int | None:
|
||||
"""Read the requested Openmart result ceiling without changing a BYOK request."""
|
||||
if not body:
|
||||
return None
|
||||
try:
|
||||
document = json.loads(body)
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
return None
|
||||
if endpoint_id in _OPENMART_LOOKUP_ENDPOINTS:
|
||||
return len(document) if isinstance(document, list) else None
|
||||
if not isinstance(document, dict):
|
||||
return None
|
||||
if endpoint_id == "openmart.companies.search":
|
||||
pagination = document.get("pagination")
|
||||
value = pagination.get("limit") if isinstance(pagination, dict) else None
|
||||
else:
|
||||
value = document.get("limit")
|
||||
return value if type(value) is int else None
|
||||
|
||||
|
||||
def _json_object(body: bytes) -> dict:
|
||||
try:
|
||||
doc = json.loads(body) if body else {}
|
||||
@@ -580,6 +619,14 @@ def _marketplace_pricing(
|
||||
"""
|
||||
if not cost:
|
||||
return 0, 0
|
||||
if provider == "openmart" and endpoint_id in _OPENMART_METERED_ENDPOINTS:
|
||||
rate = catalog_store.load().credit_rates.get("openmart")
|
||||
if rate:
|
||||
requested = _openmart_requested_records(endpoint_id, body)
|
||||
bounded = max(1, min(requested or _OPENMART_PLATFORM_MAX_RECORDS,
|
||||
_OPENMART_PLATFORM_MAX_RECORDS))
|
||||
credit_micro = _usd_to_micro(rate)
|
||||
return _openmart_credits(bounded) * credit_micro, credit_micro
|
||||
if provider == "sumble" and cost.get("sumble"):
|
||||
from . import sumble
|
||||
credit = _usd_to_micro(float(cost.get("usd") or 0) * int(cost.get("per") or 1))
|
||||
@@ -1099,6 +1146,26 @@ def _enforce_platform_request(ep: dict, body: bytes) -> None:
|
||||
has a singleton enum is the row identity, not caller choice: accepting another value lets a cheap
|
||||
row reserve for an expensive model. Full schema validation remains out of the faithful BYOK path.
|
||||
"""
|
||||
if ep.get("provider") == "openmart" and ep.get("id") in _OPENMART_METERED_ENDPOINTS:
|
||||
requested = _openmart_requested_records(ep["id"], body)
|
||||
parameter = (
|
||||
"body" if ep["id"] in _OPENMART_LOOKUP_ENDPOINTS
|
||||
else "body.pagination.limit" if ep["id"] == "openmart.companies.search"
|
||||
else "body.limit"
|
||||
)
|
||||
if requested is None or not 1 <= requested <= _OPENMART_PLATFORM_MAX_RECORDS:
|
||||
raise ResolutionFailed(
|
||||
"catalog_parameter_invalid", status_code=400, detail={
|
||||
"error": "catalog_parameter_invalid",
|
||||
"endpoint_id": ep["id"],
|
||||
"parameter": parameter,
|
||||
"message": (
|
||||
"Openmart platform calls require an explicit result count from 1 to 25; "
|
||||
"connect your own key for the upstream limit"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
input_schema = ep.get("input") or {}
|
||||
selectors: dict[str, object] = {
|
||||
path.removeprefix("body."): value
|
||||
|
||||
@@ -26,7 +26,7 @@ from ...infra.db import session_maker
|
||||
from ...models import Org
|
||||
from ...timeutil import utcnow_naive as _utcnow_naive
|
||||
from .idempotency import _release_idempotent_claim
|
||||
from .resolve import MarketplaceCall, _usd_to_micro
|
||||
from .resolve import MarketplaceCall, _openmart_credits, _usd_to_micro
|
||||
from .types import GatewayFailed, UpstreamResponse
|
||||
|
||||
|
||||
@@ -111,6 +111,29 @@ def _brightdata_record_count(body: bytes) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def _openmart_record_count(endpoint_id: str, doc: object) -> int | None:
|
||||
"""Count only the response containers verified for Openmart's synchronous data reads."""
|
||||
if endpoint_id in (
|
||||
"openmart.businesses.search",
|
||||
"openmart.companies.enrich",
|
||||
):
|
||||
if isinstance(doc, list):
|
||||
return sum(item is not None for item in doc)
|
||||
if isinstance(doc, dict) and isinstance(doc.get("data"), list):
|
||||
return sum(item is not None for item in doc["data"])
|
||||
return None
|
||||
if endpoint_id == "openmart.companies.search":
|
||||
if isinstance(doc, dict) and isinstance(doc.get("data"), list):
|
||||
return sum(item is not None for item in doc["data"])
|
||||
return None
|
||||
if endpoint_id in (
|
||||
"openmart.businesses.lookup.openmart",
|
||||
"openmart.businesses.lookup.google-place",
|
||||
):
|
||||
return sum(item is not None for item in doc.values()) if isinstance(doc, dict) else None
|
||||
return None
|
||||
|
||||
|
||||
def _quickenrich_cost_micro(mk: MarketplaceCall, doc: dict) -> int | None:
|
||||
"""Subscription credits at the frozen list rate, independent of the upstream plan fee."""
|
||||
if mk.cost_type == "free":
|
||||
@@ -300,6 +323,9 @@ def _observed_cost_micro(mk: MarketplaceCall, body: bytes, headers=None) -> int
|
||||
doc = json.loads(body)
|
||||
except (ValueError, UnicodeDecodeError):
|
||||
return 0 if provider == "contactout" else None
|
||||
if provider == "openmart" and mk.cost_type == "per_result" and mk.unit_micro > 0:
|
||||
records = _openmart_record_count(mk.endpoint_id, doc)
|
||||
return None if records is None else _openmart_credits(records) * mk.unit_micro
|
||||
if provider == "aviato" and mk.endpoint_id == "aviato.people.enrich.bulk":
|
||||
if isinstance(doc, list) and mk.unit_micro > 0:
|
||||
return sum(item is not None for item in doc) * mk.unit_micro
|
||||
|
||||
@@ -976,6 +976,16 @@ adapters:
|
||||
const: {body.category: company}
|
||||
out: {companies: results, count: "len(results)"}
|
||||
miss: "results == []"
|
||||
openmart.companies.search:
|
||||
accepts: [[q], [name]]
|
||||
in: {}
|
||||
in_expr:
|
||||
body.search_param.search_term: "coalesce(q, name)"
|
||||
body.search_param.location: "choose(country, list(obj('country', upper(country))), null)"
|
||||
body.pagination.limit: limit
|
||||
test_identity: {q: coffee shop, country: US}
|
||||
out: {companies: data, count: "len(data)", next_cursor: encoded_cursor}
|
||||
miss: "data == []"
|
||||
hunter.x.discover-companies:
|
||||
accepts: [[q]]
|
||||
in: {q: body.query}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"example_place_id":{"openmart_id":"00000000-0000-4000-8000-000000000001","name":"Example Coffee","website":"https://example.com"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"00000000-0000-4000-8000-000000000001":{"openmart_id":"00000000-0000-4000-8000-000000000001","name":"Example Coffee","website":"https://example.com"}}
|
||||
@@ -0,0 +1 @@
|
||||
{"data":["00000000-0000-4000-8000-000000000001"],"cursor":[0.98,"00000000-0000-4000-8000-000000000001"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"data":[{"openmart_id":"00000000-0000-4000-8000-000000000001","name":"Example Coffee","website":"https://example.com","country":"US"}],"cursor":[0.98,"00000000-0000-4000-8000-000000000001"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000030","task_ids":["00000000-0000-4000-8000-000000000031"]}
|
||||
@@ -0,0 +1 @@
|
||||
[{"input":"example.com","matches":[{"openmart_id":"00000000-0000-4000-8000-000000000001","name":"Example Coffee","website":"https://example.com"}]}]
|
||||
@@ -0,0 +1 @@
|
||||
{"data":[{"openmart_id":"00000000-0000-4000-8000-000000000001","name":"Example Coffee","website":"https://example.com","country":"US"}],"encoded_cursor":"ZXhhbXBsZS1jdXJzb3I="}
|
||||
@@ -0,0 +1 @@
|
||||
{"results":[{"type":"DOMAIN","value":"example.invalid","denied":false}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"created":1,"rules":[{"type":"DOMAIN","value":"example.invalid"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"deleted":1}
|
||||
@@ -0,0 +1 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000040","task_ids":["00000000-0000-4000-8000-000000000041"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000010","task_ids":["00000000-0000-4000-8000-000000000011"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"task_ids":["00000000-0000-4000-8000-000000000011"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000010","status_counts":{"COMPLETED":1,"FAILED":0,"PENDING":0,"PROCESSING":0}}
|
||||
@@ -0,0 +1 @@
|
||||
{"task_id":"00000000-0000-4000-8000-000000000011","status":"COMPLETED","result":{"people":[{"name":"Treg Example","emails":["person@example.com"],"phones":[]}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"batch_id":"00000000-0000-4000-8000-000000000020","task_ids":["00000000-0000-4000-8000-000000000021"]}
|
||||
@@ -33,6 +33,7 @@ credit_rates_usd:
|
||||
dropleads: {usd: 0.018, basis: "Pay As You Go: $18 / 1,000 non-expiring credits = $0.018 per credit. Promotional free credits do not set the platform rate", source: "https://dropleads.io/pricing", checked: "2026-09-15"}
|
||||
sumble: {usd: 0.01, basis: "Pro monthly subscription: $99 / 9900 credits = $0.01 per credit, before configured platform margin. Retain this list rate when negotiating a custom plan.", source: "https://sumble.com/pricing", checked: "2026-09-09"}
|
||||
moltsets: {usd: 0.01, kind: treg_shared_plan, fee_usd_month: 27, basis: "treg shared-plan rate: vendor charges $27/mo and the verified paid plan includes 5,000 enrichment records/week (conservatively 20,000 per four-week month); $0.01 per successful ordinary record sets break-even at 2,700 calls/mo, or 13.5% utilization, before configured platform margin.", source: "https://moltsets.com/pricing", checked: "2026-09-16"}
|
||||
openmart: {usd: 0.0298, basis: "active Starter subscription $149/mo / 5,000 credits = $0.0298 per credit; this is the replacement cost of the account treg holds, with no auto-top-up", source: "https://www.openmart.com/pricing and the Openmart account subscription", checked: "2026-09-17"}
|
||||
quickenrich: {usd: 0.004834, basis: "Starter monthly subscription: $29 / 6000 credits = $0.004833333 per credit, rounded up to 4834 micro-USD. treg base list rate before configured platform margin, assuming all monthly credits are used; unused credits increase effective cost. Free, Starter and Growth use subscription allowances. Unlimited-plan API status is not yet verified", source: "https://quickenrich.io/", checked: "2026-09-09"}
|
||||
prospeo: {usd: 0.0245, basis: "Starter monthly subscription: $49 / 2,000 credits = $0.0245 per credit, before configured platform margin and assuming all monthly credits are used; unused credits increase effective cost", source: "https://prospeo.io/pricing", checked: "2026-09-16"}
|
||||
aiark: {usd: 0.005267, basis: "Assigned monthly subscription: $79 / 15,000 credits = $0.005266666 per credit, rounded up to 5,267 micro-USD. treg base rate before configured platform margin, assuming all monthly credits are used; unused credits roll over up to twice the monthly allowance", source: "assigned AI Ark account and https://ai-ark.com/pricing", checked: "2026-09-17"}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# Openmart API. Live-verified 2026-09-17 with synthetic inputs.
|
||||
# Synchronous data reads whose price and returned-record shape are proven can use either a team's
|
||||
# own key or treg's metered platform key. Openmart rounds 0.3-credit record prices up to whole
|
||||
# credits per operation; the call runtime applies that provider-specific rule and caps shared-key
|
||||
# requests at 25 records. Async lifecycles, account mutations, and unresolved prices remain
|
||||
# BYOK-only. The shared balance endpoint is internal capacity infrastructure, not a tool.
|
||||
provider: openmart
|
||||
source:
|
||||
docs: https://app.openmart.com/api-docs
|
||||
curated: '2026-09-17'
|
||||
pricing_url: https://www.openmart.com/pricing
|
||||
limits: Search, enrich, and task reads are documented at 20 requests/second; technology tasks at 15; other batch enrichment at 25.
|
||||
proposed_capabilities:
|
||||
companies.businesses.search: Search physical business locations.
|
||||
companies.businesses.search.ids: Search and return only provider business IDs.
|
||||
companies.lookup: Retrieve company records by provider identifier.
|
||||
companies.technologies: Detect technologies used by a company.
|
||||
companies.email.find: Find company-level email addresses.
|
||||
companies.tasks.status: Read an asynchronous company-task batch status.
|
||||
companies.tasks.list: List task identifiers in an asynchronous company batch.
|
||||
companies.tasks.get: Read one asynchronous company-task result.
|
||||
companies.deny-rules.create: Add private company-account deny rules.
|
||||
companies.deny-rules.check: Check private company-account deny rules.
|
||||
companies.deny-rules.delete: Delete private company-account deny rules.
|
||||
people.find: Find people associated with a company.
|
||||
|
||||
endpoints:
|
||||
- id: openmart.businesses.search
|
||||
domain: companies
|
||||
capability: companies.businesses.search
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/search
|
||||
name: Search businesses
|
||||
summary: Search local businesses with text, location, category, and attribute filters
|
||||
input: &search_input
|
||||
bodyType: json
|
||||
body:
|
||||
query: {type: string}
|
||||
limit: {type: integer, min: 1, max: 1000}
|
||||
estimate_total: {type: boolean}
|
||||
test_request: {body: {query: treg-nonexistent-20260917, limit: 1, estimate_total: true}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Openmart support confirmed 3 credits per 10 returned records, rounded up per operation; the effective charge is ceil(0.3 × returned records), and zero records cost zero. Ten live one-result calls each deducted one whole credit. Shared-key calls require an explicit limit from 1 to 25; own-key calls keep the upstream limit.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.businesses.search.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.businesses.search.ids
|
||||
domain: companies
|
||||
capability: companies.businesses.search.ids
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/search/only_ids
|
||||
name: Search business IDs
|
||||
summary: Search businesses and return only Openmart IDs
|
||||
input: *search_input
|
||||
test_request: {body: {query: treg-nonexistent-20260917, limit: 1}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: null
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: observed
|
||||
source_url: https://app.openmart.com/api-docs
|
||||
checked: '2026-09-17'
|
||||
confidence: unknown
|
||||
note: Documentation says reduced billing; live 1-row, 10-row, and cursor calls moved no whole credit, so no fractional rate is claimed.
|
||||
platform_blocked: The upstream rate is unresolved; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.businesses.search.ids.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.businesses.lookup.openmart
|
||||
domain: companies
|
||||
capability: companies.lookup
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /api/v1/business_records/list/openmart_id
|
||||
name: Get businesses by Openmart ID
|
||||
summary: Get up to 100 full business records by Openmart ID
|
||||
input:
|
||||
bodyType: json
|
||||
note: The upstream request body is a raw JSON array of 1 to 100 Openmart ID strings.
|
||||
# Openmart documents a JSON array on GET. Keep it in the input contract, but omit it from the
|
||||
# generated smoke command because treg intentionally never prints GET commands with bodies.
|
||||
test_request: {body: [00000000-0000-4000-8000-000000000001]}
|
||||
cost: &lookup_record_cost
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: documented
|
||||
note: Full company records use the same ceil(0.3 × returned records) per-operation charge; omitted IDs are free because the response map contains no value for them. Shared-key calls accept at most 25 IDs; own-key calls keep the upstream limit.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.businesses.lookup.openmart.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.businesses.lookup.google-place
|
||||
domain: companies
|
||||
capability: companies.lookup
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: GET
|
||||
path: /api/v1/business_records/list/google_place_id
|
||||
name: Get businesses by Google Place ID
|
||||
summary: Get up to 100 full business records by Google Place ID
|
||||
input:
|
||||
bodyType: json
|
||||
note: The upstream request body is a raw JSON array of 1 to 100 Google Place ID strings.
|
||||
test_request: {body: [ChIJ00000000000000000000000]}
|
||||
cost: *lookup_record_cost
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.businesses.lookup.google-place.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.companies.enrich
|
||||
domain: companies
|
||||
capability: companies.enrich
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/enrich_company
|
||||
name: Enrich companies
|
||||
summary: Find matching business records from websites or social URLs
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
website: {type: string}
|
||||
social_media_link: {type: string}
|
||||
limit: {type: integer, min: 1, max: 50}
|
||||
estimate_total: {type: boolean}
|
||||
test_request: {body: {website: example.invalid, limit: 1}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Openmart support confirmed ceil(0.3 × returned records) per operation; a no-match returns an empty array and costs zero. Shared-key calls require an explicit limit from 1 to 25; own-key calls keep the upstream limit.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.companies.enrich.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.companies.search
|
||||
domain: companies
|
||||
capability: companies.search
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v2/brands/search
|
||||
name: Search companies
|
||||
summary: Search company brands with nested filters and cursor pagination
|
||||
input:
|
||||
bodyType: json
|
||||
body:
|
||||
search_param: {type: object, additionalProperties: true}
|
||||
pagination: {type: object, additionalProperties: true}
|
||||
test_request: {body: {search_param: {search_term: coffee shop, location: [{country: US}]}, pagination: {limit: 10}}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: record
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Openmart support confirmed ceil(0.3 × returned records) per operation; brand rows are returned in data[], and an empty page costs zero. Shared-key calls require pagination.limit from 1 to 25; own-key calls keep the upstream limit.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.companies.search.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.people.find.batch
|
||||
domain: contacts
|
||||
capability: people.find
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/find_people
|
||||
name: Find decision makers
|
||||
summary: Start up to 100 decision-maker tasks for businesses
|
||||
input: &task_batch_input
|
||||
bodyType: json
|
||||
body:
|
||||
tasks: {type: array, items: {type: object, additionalProperties: true}, minItems: 1, maxItems: 100}
|
||||
test_request: {body: {tasks: [{website: example.invalid, max_k: 1}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 11
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Email costs 3 credits and phone costs 8; requesting both can cost 11 per found person. A live both-fields task reserved 3 at submit and charged the remainder asynchronously.
|
||||
platform_blocked: Async fan-out, mixed outcomes, and delayed charges cannot be assigned safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.people.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.technologies.find.batch
|
||||
domain: companies
|
||||
capability: companies.technologies
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/find_tech
|
||||
name: Find technologies
|
||||
summary: Start up to 100 technology-detection tasks
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{website: example.invalid}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 2
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: observed
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Two credits per completed result, reconciled from the live batch and the published $6 per 100 rate.
|
||||
platform_blocked: Async batch ownership and delayed per-result charging cannot be assigned safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.technologies.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.companies.email.find.batch
|
||||
domain: contacts
|
||||
capability: companies.email.find
|
||||
platform: companies
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/lookup_business_email
|
||||
name: Find company emails
|
||||
summary: Start up to 100 company-email lookup tasks
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{website: example.invalid}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 0.3
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: documented
|
||||
note: Dashboard price is 0.3 credit per returned company email. Integer balance deltas hide fractional usage and are not settlement evidence.
|
||||
platform_blocked: Async ownership and delayed charging make this unsafe for a shared key; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.companies.email.find.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.people.enrich.batch
|
||||
domain: contacts
|
||||
capability: people.enrich
|
||||
platform: people
|
||||
scope: any_account
|
||||
method: POST
|
||||
path: /api/v1/task/batch/lookup_people
|
||||
name: Enrich known people
|
||||
summary: Start email or phone enrichment for known people
|
||||
input: *task_batch_input
|
||||
test_request: {body: {tasks: [{people: [{name: Treg Example}], info_access: [EMAIL]}]}}
|
||||
cost:
|
||||
type: per_result
|
||||
value: 11
|
||||
currency: credit
|
||||
per: 1
|
||||
unit: result
|
||||
source: docs
|
||||
source_url: https://www.openmart.com/pricing
|
||||
checked: '2026-09-17'
|
||||
confidence: verified
|
||||
note: Email costs 3 credits and phone costs 8; callers choose EMAIL, PHONE, or both.
|
||||
platform_blocked: Mixed requested fields and asynchronous per-person results cannot be settled safely; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.people.enrich.batch.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.batch.status
|
||||
domain: async
|
||||
capability: companies.tasks.status
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/batch/{batch_id}/status
|
||||
name: Get batch status
|
||||
summary: Get status counts for an Openmart batch
|
||||
input: &batch_id_input
|
||||
pathParams:
|
||||
batch_id: {type: string}
|
||||
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}}
|
||||
cost: &free_task_read {type: free, value: 0, currency: USD, unit: call, source: observed, checked: '2026-09-17', confidence: verified, note: Live task reads used no credits.}
|
||||
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.batch.status.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.batch.ids
|
||||
domain: async
|
||||
capability: companies.tasks.list
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/batch/{batch_id}/task_ids
|
||||
name: List batch task IDs
|
||||
summary: List task IDs in a batch, optionally filtered by status
|
||||
input:
|
||||
pathParams: {batch_id: {type: string}}
|
||||
query:
|
||||
status: {type: string, enum: [PENDING, PROCESSING, COMPLETED, FAILED]}
|
||||
test_request: {pathParams: {batch_id: 00000000-0000-4000-8000-000000000000}, query: {status: COMPLETED}}
|
||||
cost: *free_task_read
|
||||
platform_blocked: Batch ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.batch.ids.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.tasks.get
|
||||
domain: async
|
||||
capability: companies.tasks.get
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: GET
|
||||
path: /api/v1/task/{task_id}
|
||||
name: Get task result
|
||||
summary: Get one asynchronous task and its result
|
||||
input:
|
||||
pathParams: {task_id: {type: string}}
|
||||
test_request: {pathParams: {task_id: 00000000-0000-4000-8000-000000000000}}
|
||||
cost: *free_task_read
|
||||
platform_blocked: Task ownership is account-scoped; connect the Openmart key that created it.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.tasks.get.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.deny-rules.create
|
||||
domain: governance
|
||||
capability: companies.deny-rules.create
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: POST
|
||||
path: /api/v2/deny-rules
|
||||
name: Create deny rules
|
||||
summary: Add domains, emails, or other values to the account deny list
|
||||
input: &deny_input
|
||||
bodyType: json
|
||||
body:
|
||||
rules: {type: array, items: {type: object, additionalProperties: true}, minItems: 1}
|
||||
test_request: {body: {rules: [{type: DOMAIN, value: example.invalid}]}}
|
||||
cost: &free_account_write {type: free, value: 0, currency: USD, unit: call, source: observed, checked: '2026-09-17', confidence: verified, note: Deny-rule calls used no credits.}
|
||||
cache: forbidden
|
||||
platform_blocked: This mutates account state; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.deny-rules.create.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.deny-rules.check
|
||||
domain: governance
|
||||
capability: companies.deny-rules.check
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: POST
|
||||
path: /api/v2/deny-rules/check
|
||||
name: Check deny rules
|
||||
summary: Check values against the account deny list
|
||||
input: *deny_input
|
||||
test_request: {body: {rules: [{type: DOMAIN, value: example.invalid}]}}
|
||||
cost: *free_account_write
|
||||
cache: forbidden
|
||||
platform_blocked: The answer depends on private account state; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.deny-rules.check.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
|
||||
- id: openmart.deny-rules.delete
|
||||
domain: governance
|
||||
capability: companies.deny-rules.delete
|
||||
platform: companies
|
||||
scope: own_account
|
||||
method: DELETE
|
||||
path: /api/v2/deny-rules
|
||||
name: Delete deny rules
|
||||
summary: Delete values from the account deny list
|
||||
input: *deny_input
|
||||
test_request: {body: {rules: [{type: DOMAIN, value: example.invalid}]}}
|
||||
cost: *free_account_write
|
||||
cache: forbidden
|
||||
platform_blocked: This destructive operation mutates account state; connect your own Openmart key.
|
||||
verified: '2026-09-17'
|
||||
example_response: examples/openmart.deny-rules.delete.json
|
||||
docs_url: https://app.openmart.com/api-docs
|
||||
@@ -182,6 +182,7 @@ class Settings(BaseSettings):
|
||||
platform_key_hunter: str = ""
|
||||
platform_key_sumble: str = "" # Bearer; Pro monthly credits, optional vendor auto-top-up
|
||||
platform_key_moltsets: str = "" # Bearer; shared subscription fair-use pools, no auto-top-up
|
||||
platform_key_openmart: str = "" # Bearer; monthly subscription credits, no auto-top-up
|
||||
platform_key_harvestapi: str = "" # X-API-Key; prepaid USD wallet
|
||||
platform_key_dropleads: str = "" # X-API-Key; PAYG credits priced in fx.yaml
|
||||
platform_key_quickenrich: str = "" # Bearer; monthly subscription credits, not auto-top-up
|
||||
|
||||
@@ -130,6 +130,17 @@ async def _moltsets(c, key):
|
||||
}
|
||||
|
||||
|
||||
async def _openmart(c, key):
|
||||
d = await _get(c, "https://api.openmart.ai/api/v2/credit-balance",
|
||||
headers={"Authorization": f"Bearer {key}"})
|
||||
balance = d.get("balance") if isinstance(d, dict) else None
|
||||
if type(balance) is not int or balance < 0:
|
||||
balance = None
|
||||
period_end = d.get("period_end") if isinstance(d, dict) else None
|
||||
return {"value": balance, "unit": "credits",
|
||||
"note": f"Monthly subscription balance; current period ends {period_end or 'at the account renewal date'}."}
|
||||
|
||||
|
||||
async def _harvestapi(c, key):
|
||||
d = await _get(c, "https://api.harvestapi.io/users/my-api-user",
|
||||
headers={"X-API-Key": key})
|
||||
@@ -623,6 +634,7 @@ BALANCE_ROUTES = {
|
||||
"getleadsio": _getleadsio,
|
||||
"sumble": _sumble,
|
||||
"moltsets": _moltsets,
|
||||
"openmart": _openmart,
|
||||
"trykitt": _trykitt,
|
||||
"contactout": _contactout,
|
||||
"millionverifier": _millionverifier,
|
||||
|
||||
@@ -40,6 +40,7 @@ _KNOWN: dict[str, tuple[str, str, str]] = {
|
||||
"getleadsio": ("credits", "manual", "api"),
|
||||
"sumble": ("monthly_quota", "quota_reset", "api"),
|
||||
"moltsets": ("rolling_quota", "subscription", "api"),
|
||||
"openmart": ("credits", "subscription", "api"),
|
||||
"scrubby": ("credits", "manual", "manual"),
|
||||
"predictleads": ("monthly_quota", "quota_reset", "api"),
|
||||
"companyenrich": ("credits", "manual", "api"),
|
||||
@@ -95,6 +96,9 @@ _RATE_LIMITS: dict[str, dict] = {
|
||||
# Routing-friendly shared-key pace. The 5,000-request/5h rolling allowance is capacity, not a
|
||||
# burst rate; encoding it here would make the spacer add 3.6s before every routed attempt.
|
||||
"moltsets": {"limit": 10, "window_s": 1, "source": "policy"},
|
||||
# The provider publishes 15-25 requests/s by endpoint family. Use the strictest ceiling while
|
||||
# smoothing remains provider-wide. All direct tools are BYOK-only today.
|
||||
"openmart": {"limit": 15, "window_s": 1, "source": "docs"},
|
||||
"sumble": {"limit": 10, "window_s": 1, "source": "docs"},
|
||||
"scrubby": {"limit": 25, "window_s": 1, "source": "docs"},
|
||||
"leadsforge": {"limit": 120, "window_s": 60, "source": "headers"},
|
||||
|
||||
@@ -1310,7 +1310,10 @@ def call_template(ep: dict) -> str:
|
||||
# arguments but still requires a JSON body — and dropping `--data '{}'` from the line hands the
|
||||
# reader a command that differs from the one that was tested, on handlers that reject an empty
|
||||
# body outright.
|
||||
if body is not None and (body or ep["method"] != "GET"):
|
||||
# treg can faithfully relay a caller-supplied GET body, but generated shell commands never add
|
||||
# one: too many clients/proxies silently discard it. The stored test request can still preserve
|
||||
# the provider's unusual verification contract without printing a misleading paste-ready line.
|
||||
if body is not None and ep["method"] != "GET":
|
||||
parts += ["--data", shlex.quote(json.dumps(
|
||||
body, separators=(",", ":"), ensure_ascii=False))]
|
||||
return " ".join(parts)
|
||||
|
||||
@@ -1330,6 +1330,23 @@ MOLTSETS = OAuthProvider(
|
||||
# Live 2026-09-16: bogus Bearer 401; valid key 200. Account calls consume no records.
|
||||
)
|
||||
|
||||
OPENMART = OAuthProvider(
|
||||
service="openmart", display_name="Openmart", auth_kind="key",
|
||||
token_label="API key", token_placeholder="your Openmart API key",
|
||||
token_header="Authorization", token_format="Bearer {secret}",
|
||||
setup_url="https://app.openmart.com/",
|
||||
setup_action_label="Get your Openmart API key",
|
||||
setup_steps=("Sign in to Openmart and open the API settings.",
|
||||
"Create an API key and copy it."),
|
||||
setup_note="Searches and enrichment consume account credits. Batch tasks can charge after submission; connection verification is free.",
|
||||
auth_uri="", token_uri="", scopes={}, client_id_setting="", client_secret_setting="",
|
||||
category="Enrichment",
|
||||
summary="Search and enrich businesses, find decision makers and contact details, and detect technologies.",
|
||||
base_url="https://api.openmart.ai", docs_url="https://app.openmart.com/api-docs",
|
||||
probe_path="/api/v2/credit-balance", probe_method="GET",
|
||||
# Live 2026-09-17: missing and bogus Bearer keys returned 401; a valid key returned 200.
|
||||
)
|
||||
|
||||
HARVESTAPI = OAuthProvider(
|
||||
service="harvestapi", display_name="HarvestAPI", auth_kind="key",
|
||||
token_label="API key", token_placeholder="your HarvestAPI API key",
|
||||
@@ -3172,7 +3189,9 @@ REGISTRY: dict[str, OAuthProvider] = {
|
||||
GOOGLE_ADS, YOUTUBE,
|
||||
LINKEDIN, SLACK, X, TIKTOK, FACEBOOK, INSTAGRAM, META_ADS,
|
||||
# API-key providers
|
||||
APOLLO, PDL, AKTA, HUNTER, SUMBLE, MOLTSETS, HARVESTAPI, DROPLEADS, QUICKENRICH, PROSPEO, AIARK, WIZA, GETLEADSIO, SCRUBBY, ZEROBOUNCE, TRYKITT, CONTACTOUT, MILLIONVERIFIER, BOUNCEBAN, CRUNCHBASE, MINIMAX, OPENROUTER, REPLICATE,
|
||||
APOLLO, PDL, AKTA, HUNTER, SUMBLE, MOLTSETS, OPENMART, HARVESTAPI, DROPLEADS,
|
||||
QUICKENRICH, PROSPEO, AIARK, WIZA, GETLEADSIO, SCRUBBY, ZEROBOUNCE, TRYKITT,
|
||||
CONTACTOUT, MILLIONVERIFIER, BOUNCEBAN, CRUNCHBASE, MINIMAX, OPENROUTER, REPLICATE,
|
||||
REAPI, PIAPI,
|
||||
TIKHUB, BRIGHTDATA, SEMRUSH, JUSTONEAPI,
|
||||
SCRAPECREATORS,
|
||||
|
||||
@@ -138,6 +138,9 @@ CATALOG: list[dict] = [
|
||||
"auth": {"shape": "bearer"}},
|
||||
{"provider": "MoltSets", "tokens": ["MOLTSETS"],
|
||||
"base_url": "https://api.moltsets.com/api/v1/tools", "auth": {"shape": "bearer"}},
|
||||
{"provider": "Openmart", "tokens": ["OPENMART"],
|
||||
"base_url": "https://api.openmart.ai", "auth": {"shape": "bearer"},
|
||||
"probe": "api/v2/credit-balance"},
|
||||
{"provider": "Crustdata", "tokens": ["CRUSTDATA"], "base_url": "https://api.crustdata.com",
|
||||
"auth": {"shape": "bearer"}, "probe": "account/credits",
|
||||
"required_headers": {"x-api-version": "2025-11-01"}},
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 139 KiB |
+3
-1
@@ -53,7 +53,9 @@ for _k in (
|
||||
# without this a suite run on their laptop could resolve tier 4 and spend actual money on the
|
||||
# in-process upstream's echo. Tests that exercise tier 4 set both halves via monkeypatch.
|
||||
"PLATFORM_KEY_TRYKITT", "PLATFORM_PROVIDERS", "PLATFORM_KEY_TIKHUB", "PLATFORM_KEY_DATAFORSEO", "PLATFORM_KEY_SCRAPECREATORS",
|
||||
"PLATFORM_KEY_QUICKENRICH", "PLATFORM_KEY_PROSPEO", "PLATFORM_KEY_AIARK", "PLATFORM_KEY_WIZA", "PLATFORM_KEY_SCRUBBY", "PLATFORM_KEY_ZEROBOUNCE", "PLATFORM_KEY_SUMBLE", "PLATFORM_KEY_MOLTSETS", "PLATFORM_KEY_HARVESTAPI", "PLATFORM_KEY_DROPLEADS",
|
||||
"PLATFORM_KEY_QUICKENRICH", "PLATFORM_KEY_PROSPEO", "PLATFORM_KEY_AIARK", "PLATFORM_KEY_WIZA",
|
||||
"PLATFORM_KEY_SCRUBBY", "PLATFORM_KEY_ZEROBOUNCE", "PLATFORM_KEY_SUMBLE", "PLATFORM_KEY_MOLTSETS",
|
||||
"PLATFORM_KEY_OPENMART", "PLATFORM_KEY_HARVESTAPI", "PLATFORM_KEY_DROPLEADS",
|
||||
"PLATFORM_KEY_FINANCIALDATASETS",
|
||||
):
|
||||
os.environ[f"TREG_{_k}"] = "" # the test upstream is an in-process ASGI transport, not real DNS
|
||||
|
||||
@@ -14,6 +14,30 @@ import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
async def test_openmart_balance_collector_and_policy():
|
||||
def probe(request):
|
||||
assert request.method == "GET"
|
||||
assert request.url.path == "/api/v2/credit-balance"
|
||||
assert request.headers["authorization"] == "Bearer test"
|
||||
return httpx.Response(200, json={
|
||||
"balance": 4800,
|
||||
"period_start": "2026-09-01T00:00:00Z",
|
||||
"period_end": "2026-10-01T00:00:00Z",
|
||||
})
|
||||
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(probe)) as client:
|
||||
row = await collectors._openmart(client, "test")
|
||||
assert row == {
|
||||
"value": 4800,
|
||||
"unit": "credits",
|
||||
"note": "Monthly subscription balance; current period ends 2026-10-01T00:00:00Z.",
|
||||
}
|
||||
capacity = policy.default_policy("openmart", has_key=True)
|
||||
assert capacity.capacity_type == "credits"
|
||||
assert capacity.funding_mode == "subscription"
|
||||
assert capacity.rate_limit == {"limit": 15, "window_s": 1, "source": "docs"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value,expected", [(0, 0), (71, 71), ("5000", 5000)])
|
||||
async def test_zerobounce_balance_accepts_nonnegative_integer_values(monkeypatch, value, expected):
|
||||
monkeypatch.setenv("TREG_PLATFORM_KEY_ZEROBOUNCE", "private-test-key")
|
||||
|
||||
@@ -264,6 +264,7 @@ _UNRECORDED_SIGNATURE = {
|
||||
"bounceban", # verification credits remain; exhaustion was not forced and no overflow is claimed
|
||||
"zerobounce", # credits remain; exhaustion was not forced and no overflow is claimed
|
||||
"moltsets", # rolling allowance exhaustion was not forced; no overflow route claimed
|
||||
"openmart", # funded subscription was not exhausted; shared-key exhaustion was not forced
|
||||
# Bare 402 is already the generic balance signal; the real empty-Credits body was not forced.
|
||||
"financialdatasets",
|
||||
"minimax", "oceanio", "openrouter", "replicate", "scrapecreators", "seranking",
|
||||
|
||||
@@ -19,6 +19,40 @@ from treg.domain.catalog import store as cs
|
||||
from treg import oauth_providers as P
|
||||
|
||||
|
||||
def test_openmart_surface_separates_platform_reads_from_byok_lifecycles():
|
||||
cat = cs.load()
|
||||
rows = cat.for_provider("openmart")
|
||||
assert len(rows) == 16
|
||||
assert len({(e["method"], e["path"]) for e in rows}) == 16
|
||||
assert {e["id"] for e in rows if cat.platform_eligible(e)} == {
|
||||
"openmart.businesses.search",
|
||||
"openmart.businesses.lookup.openmart",
|
||||
"openmart.businesses.lookup.google-place",
|
||||
"openmart.companies.enrich",
|
||||
"openmart.companies.search",
|
||||
}
|
||||
assert all(e.get("verified") and e.get("example_file") for e in rows)
|
||||
assert cat.credit_rates["openmart"] == .0298
|
||||
assert "openmart.account.balance" not in cat.by_id
|
||||
|
||||
|
||||
def test_openmart_pricing_and_lifecycle_boundaries_stay_visible():
|
||||
cat = cs.load()
|
||||
assert cat.by_id["openmart.people.find.batch"]["cost"]["value"] == 11
|
||||
assert cat.by_id["openmart.technologies.find.batch"]["cost"]["value"] == 2
|
||||
company_email = cat.by_id["openmart.companies.email.find.batch"]
|
||||
assert company_email["cost"]["value"] == .3
|
||||
assert company_email["cost"]["confidence"] == "documented"
|
||||
fast = cat.by_id["openmart.businesses.search.ids"]
|
||||
assert fast["cost"]["value"] is None and fast["cost"]["confidence"] == "unknown"
|
||||
assert all(cat.by_id[key]["scope"] == "own_account" for key in (
|
||||
"openmart.tasks.batch.status", "openmart.tasks.batch.ids", "openmart.tasks.get",
|
||||
"openmart.deny-rules.create", "openmart.deny-rules.check",
|
||||
"openmart.deny-rules.delete",
|
||||
))
|
||||
assert cat.by_id["openmart.deny-rules.delete"]["cache"] == "forbidden"
|
||||
|
||||
|
||||
# ---- platform listing --------------------------------------------------------------------
|
||||
async def test_platforms_lists_the_curated_shelves_busiest_first(clients: AsyncClient):
|
||||
r = await clients.get("/catalog/platforms")
|
||||
|
||||
@@ -23,7 +23,7 @@ from treg import oauth_providers as P
|
||||
def test_key_providers_are_offerable_without_deployment_credentials():
|
||||
"""The user brings the key, so treg holds no app of its own — a key provider must be offerable,
|
||||
not shown as 'not configured' the way an unset OAuth provider is."""
|
||||
for svc in ("apollo", "pdl", "akta", "hunter", "sumble", "moltsets", "harvestapi", "dropleads", "quickenrich", "prospeo", "aiark", "wiza", "getleadsio", "scrubby", "zerobounce", "contactout", "millionverifier", "bounceban", "trykitt", "crunchbase", "tikhub", "brightdata", "semrush",
|
||||
for svc in ("apollo", "pdl", "akta", "hunter", "sumble", "moltsets", "openmart", "harvestapi", "dropleads", "quickenrich", "prospeo", "aiark", "wiza", "getleadsio", "scrubby", "zerobounce", "contactout", "millionverifier", "bounceban", "trykitt", "crunchbase", "tikhub", "brightdata", "semrush",
|
||||
"justoneapi", "dataforseo", "seranking", "moz", "majestic", "serpstat", "exa",
|
||||
"cloro",
|
||||
"lusha", "coresignal", "diffbot", "thecompaniesapi", "leadmagic", "fiber-ai",
|
||||
@@ -59,6 +59,42 @@ def test_key_providers_appear_in_the_marketplace_listing():
|
||||
assert "Market data" in P.CATEGORY_ORDER
|
||||
|
||||
|
||||
def test_openmart_registry_uses_the_free_balance_probe_and_bearer_key(monkeypatch):
|
||||
monkeypatch.setenv("TREG_PLATFORM_KEY_OPENMART", "PLATFORM-OPENMART")
|
||||
monkeypatch.setenv("TREG_PLATFORM_PROVIDERS", "openmart")
|
||||
provider = P.get("openmart")
|
||||
settings = Settings(_env_file=None)
|
||||
assert provider.base_url == "https://api.openmart.ai"
|
||||
assert provider.probe_path == "/api/v2/credit-balance"
|
||||
assert provider.probe_method == "GET"
|
||||
assert settings.platform_key_for("openmart") == "PLATFORM-OPENMART"
|
||||
assert P.platform_bindings(provider) == [{
|
||||
"platform_setting": "platform_key_openmart",
|
||||
"injector": "env",
|
||||
"location": "header",
|
||||
"name": "Authorization",
|
||||
"format": "Bearer {secret}",
|
||||
}]
|
||||
|
||||
|
||||
async def test_openmart_connect_rejects_bad_key_and_accepts_valid_key(clients, monkeypatch):
|
||||
def probe(request):
|
||||
assert request.method == "GET"
|
||||
assert request.url.path == "/api/v2/credit-balance"
|
||||
if request.headers["authorization"] == "Bearer bad":
|
||||
return httpx.Response(401, json={"detail": "Invalid API Key"})
|
||||
return httpx.Response(200, json={"balance": 4800})
|
||||
|
||||
async with AsyncClient(transport=httpx.MockTransport(probe)) as upstream:
|
||||
monkeypatch.setattr(app.state, "http", upstream)
|
||||
bad = await clients.post(
|
||||
"/connections/token", json={"provider": "openmart", "token": "bad"})
|
||||
assert bad.status_code == 422
|
||||
good = await clients.post(
|
||||
"/connections/token", json={"provider": "openmart", "token": "own-key"})
|
||||
assert good.status_code == 200, good.text
|
||||
|
||||
|
||||
def test_zerobounce_registry_uses_internal_usage_probe_and_query_key(monkeypatch):
|
||||
monkeypatch.setenv("TREG_PLATFORM_KEY_ZEROBOUNCE", "PLATFORM-ZEROBOUNCE")
|
||||
monkeypatch.setenv("TREG_PLATFORM_PROVIDERS", "zerobounce")
|
||||
|
||||
@@ -135,6 +135,15 @@ def diffbot_platform_on(monkeypatch):
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def openmart_platform_on(monkeypatch):
|
||||
monkeypatch.setenv("TREG_PLATFORM_KEY_OPENMART", "PLATFORM-OPENMART")
|
||||
monkeypatch.setenv("TREG_PLATFORM_PROVIDERS", "openmart")
|
||||
get_settings.cache_clear()
|
||||
yield
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
async def _balance(clients: AsyncClient) -> int:
|
||||
org_id = (await clients.get("/orgs")).json()[0]["org_id"]
|
||||
return (await clients.get(f"/orgs/{org_id}/balance")).json()["balance_micro"]
|
||||
@@ -802,6 +811,123 @@ def _mk(provider: str, **kw) -> call_resolution.MarketplaceCall:
|
||||
return call_resolution.MarketplaceCall(tool=None, upstream="", consumed=set(), provider=provider, **kw)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("endpoint", "body", "credits"), [
|
||||
("openmart.businesses.search", b'[]', 0),
|
||||
("openmart.businesses.search", b'[{"id":"1"}]', 1),
|
||||
("openmart.businesses.search", b'[{"id":"1"},{"id":"2"},{"id":"3"}]', 1),
|
||||
("openmart.businesses.search", b'[{},{},{},{}]', 2),
|
||||
("openmart.businesses.search", b'{"data":[{},{},{},{},{},{},{},{},{},{}]}', 3),
|
||||
("openmart.companies.enrich", b'{"data":[]}', 0),
|
||||
("openmart.companies.search", b'{"data":[{},{}]}', 1),
|
||||
("openmart.businesses.lookup.openmart", b'{"a":{},"b":{}}', 1),
|
||||
])
|
||||
def test_openmart_settlement_rounds_three_credits_per_ten_records(endpoint, body, credits):
|
||||
mk = _mk("openmart", endpoint_id=endpoint, cost_type="per_result", unit_micro=29_800)
|
||||
assert call_settle._observed_cost_micro(mk, body) == credits * 29_800
|
||||
|
||||
|
||||
def test_openmart_settlement_rejects_undocumented_response_shapes():
|
||||
search = _mk("openmart", endpoint_id="openmart.businesses.search",
|
||||
cost_type="per_result", unit_micro=29_800)
|
||||
lookup = _mk("openmart", endpoint_id="openmart.businesses.lookup.openmart",
|
||||
cost_type="per_result", unit_micro=29_800)
|
||||
assert call_settle._observed_cost_micro(search, b'{"data":{}}') is None
|
||||
assert call_settle._observed_cost_micro(lookup, b'[]') is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("endpoint", "body", "expected"), [
|
||||
("openmart.businesses.search", {"query": "coffee", "limit": 1}, 29_800),
|
||||
("openmart.businesses.search", {"query": "coffee", "limit": 4}, 59_600),
|
||||
("openmart.companies.search", {"pagination": {"limit": 25}}, 238_400),
|
||||
("openmart.businesses.lookup.openmart", ["a", "b", "c"], 29_800),
|
||||
])
|
||||
def test_openmart_reservations_use_the_same_whole_credit_rounding(endpoint, body, expected):
|
||||
cat = catalog_store.load()
|
||||
cost = cat.cost_view(cat.by_id[endpoint]["cost"], "openmart")
|
||||
estimate, unit = call_resolution._marketplace_pricing(
|
||||
"openmart", endpoint, cost, {}, json.dumps(body).encode())
|
||||
assert estimate == expected
|
||||
assert unit == 29_800
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("endpoint", "body"), [
|
||||
("openmart.businesses.search", {"query": "coffee"}),
|
||||
("openmart.businesses.search", {"query": "coffee", "limit": 26}),
|
||||
("openmart.companies.search", {"pagination": {"limit": 26}}),
|
||||
("openmart.businesses.lookup.openmart", [str(i) for i in range(26)]),
|
||||
])
|
||||
async def test_openmart_platform_calls_require_an_explicit_one_to_25_cap(
|
||||
clients, openmart_platform_on, endpoint, body,
|
||||
):
|
||||
before = await _balance(clients)
|
||||
response = await clients.request(
|
||||
catalog_store.load().by_id[endpoint]["method"], f"/call/{endpoint}",
|
||||
content=json.dumps(body), headers={"content-type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 400, response.text
|
||||
assert await _balance(clients) == before
|
||||
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
|
||||
|
||||
async def test_openmart_platform_search_settles_from_returned_rows(
|
||||
clients, openmart_platform_on, monkeypatch,
|
||||
):
|
||||
rows = [{"id": "1"}, {"id": "2"}, {"id": "3"}]
|
||||
monkeypatch.setattr(call_service, "relay", _fake_relay(200, json.dumps(rows).encode()))
|
||||
before = await _balance(clients)
|
||||
response = await clients.post("/call/openmart.businesses.search", json={
|
||||
"query": "coffee", "limit": 5,
|
||||
})
|
||||
assert response.status_code == 200, response.text
|
||||
assert await _balance(clients) == before - 29_800
|
||||
money = [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
assert [e["kind"] for e in money[:2]] == ["settle", "reserve"]
|
||||
|
||||
|
||||
async def test_openmart_byok_keeps_upstream_limits_and_is_unmetered(clients, openmart_platform_on):
|
||||
await clients.post("/secrets", json={"name": "openmart", "value": "OWN-OPENMART"})
|
||||
before = await _balance(clients)
|
||||
body = {"query": "coffee", "limit": 100}
|
||||
response = await clients.post("/call/openmart.businesses.search", json=body)
|
||||
assert response.status_code == 200, response.text
|
||||
echoed = response.json()
|
||||
assert echoed["auth"] == "Bearer OWN-OPENMART"
|
||||
assert json.loads(echoed["body"]) == body
|
||||
assert await _balance(clients) == before
|
||||
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
|
||||
|
||||
async def test_openmart_byok_lookup_preserves_the_documented_get_array_body(
|
||||
clients, openmart_platform_on,
|
||||
):
|
||||
await clients.post("/secrets", json={"name": "openmart", "value": "OWN-OPENMART"})
|
||||
ids = ["00000000-0000-4000-8000-000000000001"]
|
||||
response = await clients.request(
|
||||
"GET", "/call/openmart.businesses.lookup.openmart",
|
||||
content=json.dumps(ids), headers={"content-type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["auth"] == "Bearer OWN-OPENMART"
|
||||
assert json.loads(response.json()["body"]) == ids
|
||||
|
||||
|
||||
async def test_openmart_unpriced_fast_ids_are_blocked_before_relay_or_money(
|
||||
clients, openmart_platform_on, monkeypatch,
|
||||
):
|
||||
before = await _balance(clients)
|
||||
|
||||
async def forbidden(*args, **kwargs):
|
||||
raise AssertionError("platform guard must precede relay")
|
||||
|
||||
monkeypatch.setattr(call_service, "relay", forbidden)
|
||||
response = await clients.post("/call/openmart.businesses.search.ids", json={
|
||||
"query": "coffee", "limit": 1,
|
||||
})
|
||||
assert response.status_code == 404, response.text
|
||||
assert await _balance(clients) == before
|
||||
assert not [e for e in await _entries(clients) if e["kind"] in ("reserve", "settle", "release")]
|
||||
|
||||
|
||||
def test_observed_cost_only_trusts_a_real_number():
|
||||
"""A missing, non-numeric or negative charge means "we never learned it" — settle at the estimate.
|
||||
A reported ZERO is different: the provider is saying it did not charge, and is honoured."""
|
||||
|
||||
+3
-1
@@ -1278,7 +1278,9 @@ async def test_search_survives_missing_a_few_words_of_an_agent_sentence(clients)
|
||||
# single letters can never select: "K&L" must not let k + l decide admission, and the company
|
||||
# job ("enrich by name") must lead instead of 67 rows of noise (logged miss, 2026-08-20)
|
||||
rows, total = cs.search("K&L Gates company lookup", cat, 8)
|
||||
assert 0 < total < 30 and rows[0][0]["capability"].startswith("companies.")
|
||||
# Provider growth can add a few legitimate company-lookup rows. Keep the guard tight enough
|
||||
# to reject single-letter noise without treating new lookup providers as false positives.
|
||||
assert 0 < total < 35 and rows[0][0]["capability"].startswith("companies.")
|
||||
# the jobs rows must survive an industry qualifier the catalog never says ("law firm"), via
|
||||
# the openings->postings and firm->company aliases (logged miss, 2026-08-20)
|
||||
rows, total = cs.search("law firm job openings hiring signal", cat, 8)
|
||||
|
||||
@@ -47,7 +47,7 @@ def test_every_provider_is_registered():
|
||||
"google-ads", "youtube", "linkedin", "slack", "x", "tiktok",
|
||||
"facebook", "instagram", "meta-ads",
|
||||
# API-key providers (auth_kind="key")
|
||||
"apollo", "pdl", "akta", "hunter", "sumble", "moltsets", "harvestapi", "dropleads", "quickenrich", "prospeo", "aiark", "wiza", "getleadsio", "scrubby", "zerobounce", "contactout", "millionverifier", "bounceban", "trykitt", "crunchbase", "tikhub", "brightdata", "semrush", "justoneapi",
|
||||
"apollo", "pdl", "akta", "hunter", "sumble", "moltsets", "openmart", "harvestapi", "dropleads", "quickenrich", "prospeo", "aiark", "wiza", "getleadsio", "scrubby", "zerobounce", "contactout", "millionverifier", "bounceban", "trykitt", "crunchbase", "tikhub", "brightdata", "semrush", "justoneapi",
|
||||
"scrapecreators",
|
||||
"dataforseo", "seranking", "moz", "majestic", "serpstat", "exa",
|
||||
"cloro",
|
||||
|
||||
@@ -103,6 +103,15 @@ def test_every_shipped_adapter_round_trips_its_fixture():
|
||||
assert ad.is_miss({"email": None}) and not ad.is_miss({"email": "x"})
|
||||
|
||||
|
||||
def test_openmart_company_search_is_routed_but_company_enrich_is_not():
|
||||
cat = catalog_store.load()
|
||||
adapter = cat.adapters["openmart.companies.search"]
|
||||
assert adapter.verified, adapter.verify_note
|
||||
assert "openmart.companies.search" in cat.by_id["treg.companies.search"]["routed_children"]
|
||||
assert cat.platform_eligible(cat.by_id["openmart.companies.search"])
|
||||
assert "openmart.companies.enrich" not in cat.by_id["treg.companies.enrich"]["routed_children"]
|
||||
|
||||
|
||||
def test_dropleads_routing_surface_contains_only_verified_single_record_tools():
|
||||
catalog = catalog_store.load()
|
||||
expected = {
|
||||
|
||||
Reference in New Issue
Block a user