agentdata 0.3.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. agentdata/__init__.py +33 -0
  2. agentdata/__main__.py +4 -0
  3. agentdata/cache.py +115 -0
  4. agentdata/cli.py +173 -0
  5. agentdata/client.py +158 -0
  6. agentdata/company/__init__.py +3 -0
  7. agentdata/company/cf.py +145 -0
  8. agentdata/company/ch_zefix.py +100 -0
  9. agentdata/company/cli.py +243 -0
  10. agentdata/company/co_rues.py +72 -0
  11. agentdata/company/edgar_cik.py +84 -0
  12. agentdata/company/edgar_fts.py +101 -0
  13. agentdata/company/faces.py +746 -0
  14. agentdata/company/fdic_bankfind.py +107 -0
  15. agentdata/company/fi_ytj.py +89 -0
  16. agentdata/company/fr_recherche.py +124 -0
  17. agentdata/company/fts.py +121 -0
  18. agentdata/company/gleif.py +178 -0
  19. agentdata/company/kr_hometax.py +112 -0
  20. agentdata/company/no_brreg.py +124 -0
  21. agentdata/company/ror.py +105 -0
  22. agentdata/company/ted.py +117 -0
  23. agentdata/company/usaspending.py +124 -0
  24. agentdata/company/vies.py +66 -0
  25. agentdata/daterange.py +35 -0
  26. agentdata/economy/__init__.py +10 -0
  27. agentdata/economy/bis.py +132 -0
  28. agentdata/economy/cli.py +199 -0
  29. agentdata/economy/ecb.py +117 -0
  30. agentdata/economy/eurostat.py +163 -0
  31. agentdata/economy/faces.py +573 -0
  32. agentdata/economy/fred.py +92 -0
  33. agentdata/economy/ilo_stat.py +99 -0
  34. agentdata/economy/imf_sdmx.py +133 -0
  35. agentdata/economy/un_comtrade.py +199 -0
  36. agentdata/economy/unsd_sdg.py +154 -0
  37. agentdata/economy/wb.py +208 -0
  38. agentdata/economy/who_xmart.py +177 -0
  39. agentdata/entries.py +30 -0
  40. agentdata/errors.py +38 -0
  41. agentdata/esg/__init__.py +5 -0
  42. agentdata/esg/cli.py +180 -0
  43. agentdata/esg/climatetrace.py +260 -0
  44. agentdata/esg/eiti.py +187 -0
  45. agentdata/esg/epa_frs.py +138 -0
  46. agentdata/esg/euasl.py +75 -0
  47. agentdata/esg/eutl.py +66 -0
  48. agentdata/esg/faces.py +498 -0
  49. agentdata/esg/paris_mou.py +240 -0
  50. agentdata/esg/thetis_mrv.py +67 -0
  51. agentdata/healthcare/__init__.py +7 -0
  52. agentdata/healthcare/au_tga.py +142 -0
  53. agentdata/healthcare/ca_dpd.py +123 -0
  54. agentdata/healthcare/ca_mdall.py +155 -0
  55. agentdata/healthcare/cli.py +375 -0
  56. agentdata/healthcare/ctgov.py +101 -0
  57. agentdata/healthcare/dailymed.py +102 -0
  58. agentdata/healthcare/epmc.py +108 -0
  59. agentdata/healthcare/eudamed.py +124 -0
  60. agentdata/healthcare/faces.py +811 -0
  61. agentdata/healthcare/fda_crl.py +86 -0
  62. agentdata/healthcare/fda_devices.py +199 -0
  63. agentdata/healthcare/fda_drugs.py +156 -0
  64. agentdata/healthcare/fda_recalls.py +137 -0
  65. agentdata/healthcare/gb_mhra.py +137 -0
  66. agentdata/healthcare/gudid.py +84 -0
  67. agentdata/healthcare/mesh.py +89 -0
  68. agentdata/healthcare/rasff.py +164 -0
  69. agentdata/healthcare/rxnav.py +67 -0
  70. agentdata/healthcare/safety_gate.py +175 -0
  71. agentdata/healthcare/shortages.py +107 -0
  72. agentdata/legal/__init__.py +4 -0
  73. agentdata/legal/afdb.py +93 -0
  74. agentdata/legal/ca_sema.py +143 -0
  75. agentdata/legal/cli.py +312 -0
  76. agentdata/legal/co_sic.py +70 -0
  77. agentdata/legal/courtlistener.py +77 -0
  78. agentdata/legal/csl.py +139 -0
  79. agentdata/legal/dgcomp.py +125 -0
  80. agentdata/legal/ecfr.py +129 -0
  81. agentdata/legal/faces.py +700 -0
  82. agentdata/legal/federalregister.py +188 -0
  83. agentdata/legal/gb_cma.py +117 -0
  84. agentdata/legal/gb_tna_caselaw.py +245 -0
  85. agentdata/legal/idb.py +85 -0
  86. agentdata/legal/lda.py +147 -0
  87. agentdata/legal/sipri.py +178 -0
  88. agentdata/legal/ukec.py +148 -0
  89. agentdata/legal/wb.py +92 -0
  90. agentdata/procurement/__init__.py +5 -0
  91. agentdata/procurement/br_pncp.py +126 -0
  92. agentdata/procurement/cli.py +155 -0
  93. agentdata/procurement/co_secop.py +88 -0
  94. agentdata/procurement/faces.py +239 -0
  95. agentdata/procurement/ny_open_data.py +82 -0
  96. agentdata/procurement/ungm.py +137 -0
  97. agentdata/procurement/wb_proc.py +84 -0
  98. agentdata/research/__init__.py +4 -0
  99. agentdata/research/biorxiv.py +66 -0
  100. agentdata/research/cli.py +129 -0
  101. agentdata/research/crossref.py +121 -0
  102. agentdata/research/faces.py +349 -0
  103. agentdata/research/gb_gtr.py +58 -0
  104. agentdata/research/icite.py +52 -0
  105. agentdata/research/nih_reporter.py +143 -0
  106. agentdata/research/nsf.py +68 -0
  107. agentdata/research/openalex.py +145 -0
  108. agentdata/research/opencitations.py +50 -0
  109. agentdata/security/__init__.py +3 -0
  110. agentdata/security/cisa_kev.py +42 -0
  111. agentdata/security/cli.py +37 -0
  112. agentdata/security/faces.py +91 -0
  113. agentdata/security/osv.py +72 -0
  114. agentdata/transport.py +541 -0
  115. agentdata-0.3.26.dist-info/METADATA +266 -0
  116. agentdata-0.3.26.dist-info/RECORD +120 -0
  117. agentdata-0.3.26.dist-info/WHEEL +5 -0
  118. agentdata-0.3.26.dist-info/entry_points.txt +10 -0
  119. agentdata-0.3.26.dist-info/licenses/LICENSE +201 -0
  120. agentdata-0.3.26.dist-info/top_level.txt +1 -0
@@ -0,0 +1,126 @@
1
+ """br_pncp — Brazil national procurement portal contract face provider (pncp.gov.br /api/consulta;
2
+ operated by Serpro, keyless).
3
+
4
+ procurement domain (0.3.0 founding; admitted 2026-10-29, probed). Shape measured 2026-10-29:
5
+ GET /api/consulta/v1/contratos?dataInicial=20260901&dataFinal=20260930&pagina=1[&cnpjOrgao=…]
6
+ → {data:[…500 rows…], totalRegistros, totalPaginas, numeroPagina, paginasRestantes, empty}
7
+ — the SpringDoc spec is public (/v3/api-docs): dataInicial/dataFinal/pagina are required,
8
+ cnpjOrgao is an optional buyer filter; tamanhoPagina is a trap, do not invent it (the default
9
+ 500 is fine — the spec has the parameter but a self-made value 400s).
10
+ Row-level golden fields (2026-09 single-month window: 175,533 contracts over 352 pages, measured):
11
+ orgaoEntidade.cnpj×razaoSocial (buyer) × niFornecedor+nomeRazaoSocialFornecedor (supplier)
12
+ × the four amount columns valorInicial/Parcela/Global/Acumulado × dataAssinatura/Vigencia family
13
+ × numeroControlePNCP as the stable key × objetoContrato full text × esfera (federal/state/municipal)
14
+ × the subcontracting field family.
15
+ PII red line (precedent: fda_crl): on tipoPessoa=PF rows the niFornecedor (CPF) and the
16
+ supplier's personal name are not returned — set to None + fornecedorMascarado:true stated
17
+ explicitly; tipoPessoa=PJ (incl. MEI) is returned as upstream has it (CNPJ = public business
18
+ registry key).
19
+ Date format: compact YYYYMMDD (the dashed YYYY-MM-DD form is accepted and normalized to
20
+ compact before sending — both forms measured 200 on 2026-10-29).
21
+ TTL: contract-window history months are stable, recent windows change slowly — 7 days; polite
22
+ tier 1 rps (Serpro publishes no quota, so take the conservative value).
23
+ """
24
+ from ..errors import SdkError
25
+
26
+ provider = {
27
+ "id": "br_pncp",
28
+ "baseUrl": "https://pncp.gov.br",
29
+ "rate": {"rps": 1, "burst": 2},
30
+ "ops": {
31
+ "contracts": {"path": "/api/consulta/v1/contratos"}, # params: dataInicial/dataFinal/pagina (cnpjOrgao optional)
32
+ },
33
+ "ttl": {"contracts": 7 * 86400},
34
+ }
35
+
36
+
37
+ def _mask_supplier(row, tipo_pessoa, ni_key, nome_key):
38
+ """PII red line: mask the document number and the personal name on PF (individual) rows;
39
+ PJ rows are returned as-is. Sets None + an explicit flag (never silent)."""
40
+ masked = str(tipo_pessoa or "").strip().upper() == "PF"
41
+ if masked:
42
+ row[ni_key] = None
43
+ row[nome_key] = None
44
+ row["fornecedorMascarado"] = masked
45
+ return row
46
+
47
+
48
+ def contracts_from_upstream(data):
49
+ """Upstream contratos response → contract rows (pure function, offline-testable; PII masking
50
+ happens at this layer)."""
51
+ rows = []
52
+ for r in (data or {}).get("data") or []:
53
+ org = r.get("orgaoEntidade") or {}
54
+ unidade = r.get("unidadeOrgao") or {}
55
+ row = {
56
+ "numeroControlePNCP": r.get("numeroControlePNCP"), # stable key (the portal's contract identifier)
57
+ "anoContrato": r.get("anoContrato"),
58
+ "categoria": (r.get("categoriaProcesso") or {}).get("nome"),
59
+ "tipoContrato": (r.get("tipoContrato") or {}).get("nome"),
60
+ "orgaoEntidade": {"cnpj": org.get("cnpj"), "razaoSocial": org.get("razaoSocial"),
61
+ "esfera": org.get("esferaId"), "poder": org.get("poderId")},
62
+ "unidadeOrgao": {"uf": unidade.get("ufSigla"), "municipio": unidade.get("municipioNome")},
63
+ "tipoPessoa": r.get("tipoPessoa"), # PJ/PF (PF = supplier is an individual)
64
+ "niFornecedor": r.get("niFornecedor"), # supplier CNPJ/CPF (masked for PF)
65
+ "nomeRazaoSocialFornecedor": r.get("nomeRazaoSocialFornecedor"), # supplier name (masked for PF)
66
+ "valorInicial": r.get("valorInicial"),
67
+ "valorParcela": r.get("valorParcela"),
68
+ "valorGlobal": r.get("valorGlobal"),
69
+ "valorAcumulado": r.get("valorAcumulado"),
70
+ "dataAssinatura": r.get("dataAssinatura"),
71
+ "dataVigenciaInicio": r.get("dataVigenciaInicio"),
72
+ "dataVigenciaFim": r.get("dataVigenciaFim"),
73
+ "dataPublicacaoPncp": r.get("dataPublicacaoPncp"),
74
+ "numeroRetificacao": r.get("numeroRetificacao"),
75
+ "objetoContrato": r.get("objetoContrato"), # full text of the contract subject
76
+ }
77
+ _mask_supplier(row, r.get("tipoPessoa"), "niFornecedor", "nomeRazaoSocialFornecedor")
78
+ sub_tipo = r.get("tipoPessoaSubContratada")
79
+ if sub_tipo or r.get("niFornecedorSubContratado") or r.get("nomeFornecedorSubContratado"):
80
+ row["subContratado"] = _mask_supplier(
81
+ {"tipoPessoa": sub_tipo, "niFornecedor": r.get("niFornecedorSubContratado"),
82
+ "nomeFornecedor": r.get("nomeFornecedorSubContratado")},
83
+ sub_tipo, "niFornecedor", "nomeFornecedor")
84
+ rows.append(row)
85
+ return {
86
+ "rows": rows, "count": len(rows),
87
+ "total": data.get("totalRegistros") if isinstance(data, dict) else None,
88
+ "total_paginas": data.get("totalPaginas") if isinstance(data, dict) else None,
89
+ "paginas_restantes": data.get("paginasRestantes") if isinstance(data, dict) else None,
90
+ }
91
+
92
+
93
+ def _norm_date(v, field):
94
+ s = str(v or "").strip()
95
+ if len(s) == 10 and s[4] == "-" and s[7] == "-":
96
+ s = s.replace("-", "") # YYYY-MM-DD → compact (both forms measured 200, compact is what gets sent)
97
+ if len(s) != 8 or not s.isdigit():
98
+ raise SdkError("usage_error", provider="br_pncp",
99
+ detail=f"{field} must be YYYYMMDD or YYYY-MM-DD (contract publication-window endpoint), got '{v}'")
100
+ return s
101
+
102
+
103
+ def contracts_raw(data_inicial, data_final, *, pagina=1, cnpj_orgao=None, cache=None, no_cache=False):
104
+ """→ {rows[], count, total, total_paginas, paginas_restantes, params, _meta}.
105
+
106
+ data_inicial/data_final = the publication window (required, upstream names verbatim);
107
+ pagina pages from 1 (500 rows per page = the upstream default); cnpj_orgao = buyer CNPJ
108
+ 14-digit filter (optional)."""
109
+ from ..transport import call_provider
110
+
111
+ di, df = _norm_date(data_inicial, "data_inicial"), _norm_date(data_final, "data_final")
112
+ if di > df:
113
+ raise SdkError("usage_error", provider="br_pncp",
114
+ detail=f"data_inicial ({di}) must not be later than data_final ({df})")
115
+ pg = int(pagina) if pagina is not None else 1
116
+ if pg < 1:
117
+ raise SdkError("usage_error", provider="br_pncp", detail=f"pagina must be a page number ≥1, got '{pagina}'")
118
+ params = {"dataInicial": di, "dataFinal": df, "pagina": pg}
119
+ orgao = str(cnpj_orgao or "").strip().replace("/", "").replace(".", "").replace("-", "") or None
120
+ if orgao:
121
+ if len(orgao) != 14 or not orgao.isdigit():
122
+ raise SdkError("usage_error", provider="br_pncp",
123
+ detail=f"cnpj_orgao must be the buyer's 14-digit CNPJ, got '{cnpj_orgao}'")
124
+ params["cnpjOrgao"] = orgao
125
+ r = call_provider(provider, "contracts", params, cache=cache, no_cache=no_cache)
126
+ return {**contracts_from_upstream(r["data"]), "params": params, "_meta": r}
@@ -0,0 +1,155 @@
1
+ """procurement cli — domain command-face registration (seventh package, stood up in 0.3.0, born flat — no verb layer).
2
+
3
+ register(v, g): registers flat commands onto the domain subparser (g = root _leaf_globals factory); FLAT = flat dispatch
4
+ table, merged by the root cli for main to use (same shape as the 0.2.6 per-domain split). Face order = lexicographic by
5
+ face name (sorting source of truth = the English identifier).
6
+ """
7
+ import argparse
8
+
9
+ def register(v, g):
10
+ # procurement 0.3.0 founding pair (pncp_contracts/co_secop_contracts) + 0.3.6 World Bank + New York State batch
11
+ # (wb_proc/ny_open_data; upstream param names mirrored) — all folded in lexicographically by 0.3.10; 0.3.12 collection
12
+ # round adds the UNGM/NY OGS batch (ungm/ny_open_data) under the same law; 0.3.13 ISO country-prefix machine batch
13
+ # renames pncp_contracts -> br_pncp_contracts and secop_contracts -> co_secop_contracts (provider ids follow the module names)
14
+
15
+ f1 = v.add_parser("br_pncp_contracts", parents=[g()],
16
+ help="Brazil public-procurement contracts (PNCP, keyless)",
17
+ description="Brazil national procurement contracts (Portal Nacional de Contratações Públicas, "
18
+ "keyless): publication-window search over /api/consulta/v1/contratos — every row is "
19
+ "buyer CNPJ × supplier CNPJ with the four valor columns; join straight onto the RFB "
20
+ "register. data_inicial/data_final (YYYYMMDD or YYYY-MM-DD) and pagina are upstream "
21
+ "verbatim (500 rows/page); cnpj_orgao narrows to one buyer. PF (individual) supplier "
22
+ "rows carry their identifier and name masked per the PII rule")
23
+ f1.add_argument("--data-inicial", required=True, help="Publication-window start (upstream param: dataInicial; YYYYMMDD or YYYY-MM-DD)")
24
+ f1.add_argument("--data-final", required=True, help="Publication-window end (upstream param: dataFinal)")
25
+ f1.add_argument("--pagina", type=int, default=1, help="Page number (upstream param: pagina, 1-based; 500 rows/page)")
26
+ f1.add_argument("--cnpj-orgao", default=None, help="Buyer CNPJ 14-digit filter (upstream param: cnpjOrgao)")
27
+
28
+ f2 = v.add_parser("co_secop_contracts", parents=[g()],
29
+ help="Colombia SECOP II electronic contracts (keyless)",
30
+ description="Colombia SECOP II electronic contracts (datos.gov.co Socrata dataset jbjy-vk9h, "
31
+ "keyless): 5.6M+ contracts — buyer NIT × supplier NIT/CC with the valor family; join "
32
+ "straight onto the RUES register. where is the raw SoQL $where channel (verbatim "
33
+ "pass-through); limit/offset/order mirror $limit/$offset/$order. No total echo — page "
34
+ "with offset. Personal-document (Cédula etc.) supplier rows carry their identifier "
35
+ "and name masked per the PII rule")
36
+ f2.add_argument("--where", default=None, help="Raw SoQL filter (upstream param: $where; e.g. \"upper(nombre_entidad) like '%%MINISTERIO%%'\")") # %% escaping: argparse help goes through % interpolation
37
+ f2.add_argument("--limit", type=int, default=20, help="Rows (upstream param: $limit; 1-500 politeness cap)")
38
+ f2.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: $offset; Socrata has no total echo)")
39
+ f2.add_argument("--order", default=None, help="Sort (upstream param: $order; column [ASC|DESC] — no NULLS syntax)")
40
+
41
+ f3 = v.add_parser("ny_abo_contracts", parents=[g()],
42
+ help="New York State authority contracts (ABO, keyless)",
43
+ description="New York State authority contracts (data.ny.gov Socrata dataset ehig-g5x3, "
44
+ "keyless): 275k+ rows from the Authorities Budget Office universe — buyer "
45
+ "authority_name × vendor_name + full address (no UEI/DUNS; name+address walk), "
46
+ "four value columns plus MWBE / NYS-business preference flags. where is the raw SoQL "
47
+ "$where channel; limit/offset/order mirror $limit/$offset/$order. No total echo — "
48
+ "page with offset")
49
+ f3.add_argument("--where", default=None, help="Raw SoQL filter (upstream param: $where; e.g. \"upper(vendor_name) like '%%DELTA%%'\")")
50
+ f3.add_argument("--limit", type=int, default=20, help="Rows (upstream param: $limit; 1-500 politeness cap)")
51
+ f3.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: $offset; Socrata has no total echo)")
52
+ f3.add_argument("--order", default=None, help="Sort (upstream param: $order; column [ASC|DESC] — no NULLS syntax)")
53
+
54
+ f4 = v.add_parser("ny_mta_procurements", parents=[g()],
55
+ help="New York MTA procurements (keyless)",
56
+ description="New York MTA procurements (data.ny.gov Socrata dataset twsw-2mqa, keyless): "
57
+ "107k+ rows — buyer is always the MTA; vendor_name + value family + "
58
+ "vendor_is_nys_or_fbe preference flag. where is the raw SoQL $where channel; "
59
+ "limit/offset/order mirror $limit/$offset/$order. No total echo — page with offset")
60
+ f4.add_argument("--where", default=None, help="Raw SoQL filter (upstream param: $where; e.g. \"upper(vendor_name) like '%%SKANSKA%%'\")")
61
+ f4.add_argument("--limit", type=int, default=20, help="Rows (upstream param: $limit; 1-500 politeness cap)")
62
+ f4.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: $offset; Socrata has no total echo)")
63
+ f4.add_argument("--order", default=None, help="Sort (upstream param: $order; column [ASC|DESC] — no NULLS syntax)")
64
+
65
+ f5 = v.add_parser("ny_ogs_payments", parents=[g()],
66
+ help="New York OGS design & construction payments (keyless)",
67
+ description="New York State Office of General Services design & construction payments "
68
+ "(data.ny.gov Socrata dataset rb9h-9fit, keyless): 69k+ payment rows — "
69
+ "buyer is always OGS; each row is contractnumber × vendor × paymentamount "
70
+ "with fiscalyear/quarter/county (no address columns — name walk). Completes "
71
+ "the New York trio (ABO authorities / MTA transport / OGS construction) "
72
+ "buyer universe. where is the raw SoQL $where channel; limit/offset/order "
73
+ "mirror $limit/$offset/$order. No total echo — page with offset")
74
+ f5.add_argument("--where", default=None, help="Raw SoQL filter (upstream param: $where; e.g. \"upper(vendor) like '%%HIGHLAND%%'\")")
75
+ f5.add_argument("--limit", type=int, default=20, help="Rows (upstream param: $limit; 1-500 politeness cap)")
76
+ f5.add_argument("--offset", type=int, default=0, help="Row offset (upstream param: $offset; Socrata has no total echo)")
77
+ f5.add_argument("--order", default=None, help="Sort (upstream param: $order; column [ASC|DESC] — no NULLS syntax)")
78
+
79
+ f6 = v.add_parser("ungm_contract_awards", parents=[g()],
80
+ help="UN Global Marketplace contract-award search (keyless)",
81
+ description="United Nations Global Marketplace contract awards (www.ungm.org public "
82
+ "search, keyless): ~50k awards across the UN-agency buyer universe "
83
+ "(UNDP/UNFPA/ILO/UNOPS...) × global vendors — each row is award id + "
84
+ "title/vendor/award date/agency/reference/supplier country. supplier/title/"
85
+ "reference/award_from/award_to/countries/agencies/unspscs are upstream "
86
+ "verbatim; page_index (1-based) and page_size (1-100, site default 15) "
87
+ "page through; newest first by default (sort_field=AwardDate, "
88
+ "sort_ascending=false). The ASP.NET anti-forgery session pairing is "
89
+ "handled internally (15-minute reuse, auto re-bootstrap on expiry)")
90
+ f6.add_argument("--supplier", default=None, help="Vendor name filter (upstream param: Supplier; e.g. DELTA)")
91
+ f6.add_argument("--title", default=None, help="Title text filter (upstream param: Title)")
92
+ f6.add_argument("--reference", default=None, help="Reference filter (upstream param: Reference; e.g. rfx_11130_ROAP)")
93
+ f6.add_argument("--award-from", default=None, help="Award date window start (upstream param: AwardFrom; site picker format dd-MMM-yyyy)")
94
+ f6.add_argument("--award-to", default=None, help="Award date window end (upstream param: AwardTo)")
95
+ f6.add_argument("--countries", nargs="+", default=None, help="Project-country filter list (upstream param: Countries[])")
96
+ f6.add_argument("--agencies", nargs="+", default=None, help="UN-agency filter list (upstream param: Agencies[]; e.g. UNDP ILO)")
97
+ f6.add_argument("--unspscs", nargs="+", default=None, help="UNSPSC code filter list (upstream param: UNSPSCs[])")
98
+ f6.add_argument("--page-index", type=int, default=1, help="Page number (upstream param: PageIndex, 1-based)")
99
+ f6.add_argument("--page-size", type=int, default=15, help="Page size (upstream param: PageSize; 1-100 politeness cap, site default 15)")
100
+ f6.add_argument("--sort-field", default="AwardDate", help="Sort field (upstream param: SortField; default AwardDate — never empty: null makes the upstream 500)")
101
+ f6.add_argument("--sort-ascending", action="store_true", help="Sort ascending (upstream param: SortAscending; default false = newest first)")
102
+
103
+ f7 = v.add_parser("wb_procnotices", parents=[g()],
104
+ help="World Bank procurement notices (keyless)",
105
+ description="World Bank procurement notice stream (search.worldbank.org /api/v2/procnotices, "
106
+ "keyless): 313k+ notices across the MDB buyer universe (IBRD/IDA projects in 100+ "
107
+ "countries). notice_type/rows/os are upstream verbatim (default: Contract Award); "
108
+ "supplier keys are extracted from the embedded award section into "
109
+ "awarded_firms[]{name, supp_id} — firm name + WB supplier id, joinable onto the WB "
110
+ "debarred list. notice_text itself is not returned (embedded contact details stay "
111
+ "out of the face)")
112
+ f7.add_argument("--notice-type", default="Contract Award", help="Notice type (upstream param: notice_type; e.g. Contract Award, General Procurement Notice)")
113
+ f7.add_argument("--rows", type=int, default=10, help="Page size (upstream param: rows; 1-100 politeness cap)")
114
+ f7.add_argument("--os", type=int, default=0, help="Offset (upstream param: os)")
115
+
116
+
117
+ FLAT = {
118
+ # procurement flat panel (upstream param names verbatim + our defaults; lambdas only shuttle names across namespaces).
119
+ # 0.3.6 additions: the World Bank notice stream + the NY Socrata twin faces; 0.3.12 collection additions: the UNGM award
120
+ # search + the NY OGS third face; 0.3.13 renames pncp_contracts -> br_pncp_contracts, secop_contracts -> co_secop_contracts
121
+ # — all folded in lexicographically
122
+ "br_pncp_contracts": lambda a: {"data_inicial": a.data_inicial, "data_final": a.data_final,
123
+ "pagina": a.pagina, "cnpj_orgao": a.cnpj_orgao},
124
+ "co_secop_contracts": lambda a: {"where": a.where, "limit": a.limit,
125
+ "offset": a.offset, "order": a.order},
126
+ "ny_abo_contracts": lambda a: {"where": a.where, "limit": a.limit,
127
+ "offset": a.offset, "order": a.order},
128
+ "ny_mta_procurements": lambda a: {"where": a.where, "limit": a.limit,
129
+ "offset": a.offset, "order": a.order},
130
+ "ny_ogs_payments": lambda a: {"where": a.where, "limit": a.limit,
131
+ "offset": a.offset, "order": a.order},
132
+ "ungm_contract_awards": lambda a: {"supplier": a.supplier, "title": a.title,
133
+ "reference": a.reference, "award_from": a.award_from,
134
+ "award_to": a.award_to, "countries": a.countries,
135
+ "agencies": a.agencies, "unspscs": a.unspscs,
136
+ "page_index": a.page_index, "page_size": a.page_size,
137
+ "sort_field": a.sort_field, "sort_ascending": a.sort_ascending},
138
+ "wb_procnotices": lambda a: {"notice_type": a.notice_type, "rows": a.rows, "os": a.os},
139
+ }
140
+
141
+
142
+ # 0.3.10 jurisdiction chapter: face -> jurisdiction (default = the issuing authority's jurisdiction; institutions
143
+ # covering global phenomena = GLOBAL; vocabulary ISO-3166 alpha-2 + EU + GLOBAL) — merged by the root cli.py
144
+ # into _JURISDICTION, the success envelope carries a jurisdiction chapter;
145
+ # 0.3.12 collection: +ungm_contract_awards=GLOBAL (a global UN institution), +ny_ogs_payments=US;
146
+ # 0.3.13 renames pncp_contracts -> br_pncp_contracts, secop_contracts -> co_secop_contracts (jurisdictions unchanged)
147
+ COUNTRY = {
148
+ "br_pncp_contracts": "BR",
149
+ "co_secop_contracts": "CO",
150
+ "ny_abo_contracts": "US",
151
+ "ny_mta_procurements": "US",
152
+ "ny_ogs_payments": "US",
153
+ "ungm_contract_awards": "GLOBAL",
154
+ "wb_procnotices": "GLOBAL",
155
+ }
@@ -0,0 +1,88 @@
1
+ """co_secop — Colombia SECOP II electronic contract face provider (datos.gov.co Socrata dataset
2
+ jbjy-vk9h; keyless).
3
+
4
+ procurement domain (0.3.0 founding; admitted 2026-10-29, probed). Shape measured 2026-10-29:
5
+ GET /resource/jbjy-vk9h.json?$limit=2&$order=ultima_actualizacion DESC
6
+ → [rows…88 columns…] (Socrata returns an array directly, no total echo — paging goes through
7
+ $offset; the corpus-wide count is probed separately via $select=count)
8
+ — the Colombian procurement corpus holds **5,665,176 electronic contracts** (count measured
9
+ 2026-10-29).
10
+ Row-level golden fields: nit_entidad (buyer NIT) × documento_proveedor+proveedor_adjudicado
11
+ (supplier NIT/CC) × the amount family valor_del_contrato/facturado/pagado/pendiente_de_pago
12
+ × departamento/ciudad/sector × id_contrato as the stable key × the date family
13
+ fecha_de_firma/inicio/fin × es_pyme — joins straight onto the RUES register by NIT.
14
+ PII red line (precedent: fda_crl): on rows whose tipodocproveedor is a personal-document type
15
+ (Cédula de Ciudadanía/Extranjería, Pasaporte, NUIP, Tarjeta de Identidad) the
16
+ documento_proveedor and the supplier's personal name are not returned — set to None +
17
+ proveedorMascarado:true stated explicitly; NIT (company tax id) rows are returned as-is.
18
+ Socrata $-parameter channel, verbatim names: $where raw SoQL (same shape as dgcomp's raw DSL
19
+ precedent — passed through, no local wall) / $limit/$offset/$order; the $order value domain is
20
+ column[ ASC|DESC] (measured 2026-10-29: the "… DESC NULL LAST" form 400s — SoQL does not
21
+ accept the NULLS syntax).
22
+ TTL: contract rows change slowly with ultima_actualizacion — 24h; polite tier 1 rps (anonymous
23
+ Socrata throttling, take the conservative value).
24
+ """
25
+ from ..errors import SdkError
26
+
27
+ provider = {
28
+ "id": "co_secop",
29
+ "baseUrl": "https://datos.gov.co",
30
+ "rate": {"rps": 1, "burst": 2},
31
+ "ops": {
32
+ "contracts": {"path": "/resource/jbjy-vk9h.json"}, # Socrata $limit/$offset/$where/$order
33
+ },
34
+ "ttl": {"contracts": 24 * 3600},
35
+ }
36
+
37
+ # personal-document types (masked); NIT = company tax id, returned as-is
38
+ _PERSONAL_DOC = {"cédula de ciudadanía", "cédula de extranjería", "pasaporte",
39
+ "nuip", "tarjeta de identidad", "cedula de ciudadania", "cedula de extranjeria"}
40
+
41
+ # row narrowing (88 columns → bilateral + amount + status skeleton; upstream column names verbatim)
42
+ _FIELDS = ("nombre_entidad", "nit_entidad", "departamento", "ciudad", "orden", "sector", "rama",
43
+ "proceso_de_compra", "id_contrato", "estado_contrato", "tipo_de_contrato",
44
+ "modalidad_de_contratacion", "fecha_de_firma", "fecha_de_inicio_del_contrato",
45
+ "fecha_de_fin_del_contrato", "tipodocproveedor", "documento_proveedor",
46
+ "proveedor_adjudicado", "es_pyme", "valor_del_contrato", "valor_facturado",
47
+ "valor_pagado", "valor_pendiente_de_pago")
48
+
49
+
50
+ def contracts_from_upstream(data):
51
+ """Upstream SODA row array → contract rows (pure function, offline-testable; PII masking
52
+ happens at this layer)."""
53
+ rows = []
54
+ for r in data or []:
55
+ row = {k: r.get(k) for k in _FIELDS}
56
+ masked = str(r.get("tipodocproveedor") or "").strip().lower() in _PERSONAL_DOC
57
+ if masked:
58
+ row["documento_proveedor"] = None
59
+ row["proveedor_adjudicado"] = None
60
+ row["proveedorMascarado"] = masked
61
+ rows.append(row)
62
+ return {"rows": rows, "count": len(rows)}
63
+
64
+
65
+ def contracts_raw(where=None, *, limit=20, offset=0, order=None, cache=None, no_cache=False):
66
+ """→ {rows[], count, limit_applied, params, _meta} (Socrata sends no total echo — paging
67
+ goes through offset).
68
+
69
+ where = $where raw SoQL (e.g. "upper(proveedor_adjudicado) like '%ECOPETROL%'"); limit/$limit
70
+ politeness cap 1-500; offset/$offset ≥0; order/$order = column[ ASC|DESC]."""
71
+ from ..transport import call_provider
72
+
73
+ size = int(limit) if limit is not None else 20 # explicit None check — an `or default` swallows 0, the same trap as 0.2.10
74
+ if not 1 <= size <= 500:
75
+ raise SdkError("usage_error", provider="co_secop",
76
+ detail=f"limit must be 1-500 (politeness cap), got '{limit}'")
77
+ off = int(offset) if offset is not None else 0
78
+ if off < 0:
79
+ raise SdkError("usage_error", provider="co_secop", detail=f"offset must be ≥0, got '{offset}'")
80
+ params = {"$limit": size, "$offset": off}
81
+ w = str(where or "").strip() or None
82
+ if w:
83
+ params["$where"] = w
84
+ o = str(order or "").strip() or None
85
+ if o:
86
+ params["$order"] = o
87
+ r = call_provider(provider, "contracts", params, cache=cache, no_cache=no_cache)
88
+ return {**contracts_from_upstream(r["data"]), "limit_applied": size, "params": params, "_meta": r}
@@ -0,0 +1,239 @@
1
+ """procurement faces — flat face-method mixin (0.3.0 seventh package, born flat).
2
+
3
+ Mixes into the root _Domain (assembled via MRO, see client.py); discipline: no __init__,
4
+ depend only on root internals (_flat_env/_c/_request…), method names globally unique across
5
+ domains (a collision would shadow another domain's face).
6
+ """
7
+ from . import ny_open_data, br_pncp, co_secop, ungm, wb_proc
8
+ from ..errors import SdkError
9
+
10
+
11
+ class ProcurementFaces:
12
+ def _pr_only(self, tool: str):
13
+ if self.name != "procurement":
14
+ raise SdkError("usage_error", detail=f"{tool} is a procurement-domain capability (not available on the {self.name} domain)")
15
+
16
+ def br_pncp_contracts(self, data_inicial, data_final, *, pagina=1, cnpj_orgao=None,
17
+ execution=None, no_cache=None):
18
+ """procurement br_pncp_contracts: Brazil national procurement contract window
19
+ (PNCP /api/consulta/v1/contratos; data_inicial/data_final/pagina mirrored verbatim,
20
+ cnpj_orgao optional buyer filter — pinned by the SpringDoc spec).
21
+ Rows = buyer CNPJ × supplier CNPJ bilateral + the four amount columns; PF supplier rows
22
+ are masked per the PII red line."""
23
+ self._pr_only("br_pncp_contracts")
24
+ r = br_pncp.contracts_raw(data_inicial, data_final, pagina=pagina, cnpj_orgao=cnpj_orgao,
25
+ cache=self._c._cache, no_cache=no_cache if no_cache is not None else self._c.no_cache)
26
+ res = {k: v for k, v in r.items() if k != "_meta"}
27
+ rows = res["rows"]
28
+ n_masked = sum(1 for x in rows if x.get("fornecedorMascarado"))
29
+ org = rows[0]["orgaoEntidade"] if rows else {}
30
+ w = f"{r['params']['dataInicial']}–{r['params']['dataFinal']}"
31
+ if rows:
32
+ reason = (f"PNCP contract window {w}: {res['total']:,} contracts in the window over "
33
+ f"{res['total_paginas']} pages (500 rows/page), page {pagina} returned {len(rows)} rows "
34
+ f"(sample buyer {org.get('razaoSocial') or '—'}) — buyer × supplier CNPJ joins straight onto the RFB register"
35
+ + (f"; {n_masked} PF (individual) supplier rows masked per the PII red line" if n_masked else ""))
36
+ else:
37
+ reason = f"PNCP contract window {w}: page {pagina} returned 0 rows — an empty window or an out-of-range page is a legitimate answer (total={res['total']})"
38
+ ev = [{"quote": f"{x['numeroControlePNCP']} — {x['orgaoEntidade'].get('razaoSocial') or '—'} × "
39
+ f"{x.get('nomeRazaoSocialFornecedor') or '(PF masked)'} — "
40
+ f"{x.get('valorGlobal')} BRL",
41
+ "url": "https://pncp.gov.br/api/consulta/v1/contratos"}
42
+ for x in rows[:3] if x.get("numeroControlePNCP")]
43
+ return self._flat_env(tool="br_pncp_contracts",
44
+ input={"data_inicial": data_inicial, "data_final": data_final,
45
+ "pagina": pagina, "cnpj_orgao": cnpj_orgao},
46
+ reason=reason, result=res, evidence=ev, provider_id="br_pncp",
47
+ metas=[r["_meta"]],
48
+ query={"source": "raw", "upstream": r["params"]})
49
+
50
+ def co_secop_contracts(self, where=None, *, limit=20, offset=0, order=None,
51
+ execution=None, no_cache=None):
52
+ """procurement co_secop_contracts: Colombia SECOP II electronic contracts (datos.gov.co
53
+ Socrata jbjy-vk9h; where→$where raw SoQL verbatim channel, limit/offset/order mirrored
54
+ by the same names). 5.66M contracts corpus-wide; rows = buyer NIT × supplier NIT
55
+ bilateral + the amount family; personal-document supplier rows are masked per the PII
56
+ red line."""
57
+ self._pr_only("co_secop_contracts")
58
+ r = co_secop.contracts_raw(where, limit=limit, offset=offset, order=order,
59
+ cache=self._c._cache, no_cache=no_cache if no_cache is not None else self._c.no_cache)
60
+ res = {k: v for k, v in r.items() if k != "_meta"}
61
+ rows = res["rows"]
62
+ n_masked = sum(1 for x in rows if x.get("proveedorMascarado"))
63
+ if rows:
64
+ reason = (f"SECOP II electronic contracts: {len(rows)} rows returned (from offset={offset}; "
65
+ f"Socrata sends no total echo) — buyer NIT × supplier NIT joins straight onto "
66
+ f"the RUES register (sample {rows[0].get('nombre_entidad') or '—'})"
67
+ + (f"; {n_masked} personal-document (CC etc.) supplier rows masked per the PII red line" if n_masked else ""))
68
+ else:
69
+ reason = "SECOP II: 0 rows — either the where filter has no match or offset is past the end; both are legitimate answers (not errors)"
70
+ ev = [{"quote": f"{x.get('id_contrato')} — {x.get('nombre_entidad') or '—'} × "
71
+ f"{x.get('proveedor_adjudicado') or '(personal masked)'} — "
72
+ f"{x.get('valor_del_contrato')} COP",
73
+ "url": "https://datos.gov.co/resource/jbjy-vk9h.json"}
74
+ for x in rows[:3] if x.get("id_contrato")]
75
+ return self._flat_env(tool="co_secop_contracts",
76
+ input={"where": where, "limit": limit, "offset": offset, "order": order},
77
+ reason=reason, result=res, evidence=ev, provider_id="co_secop",
78
+ metas=[r["_meta"]],
79
+ query={"source": "raw", "upstream": r["params"]})
80
+
81
+ def wb_procnotices(self, notice_type="Contract Award", *, rows=10, os=0,
82
+ execution=None, no_cache=None):
83
+ """procurement wb_procnotices: World Bank procurement notice stream (search.worldbank.org /api/v2/procnotices;
84
+ notice_type/rows/os mirrored verbatim, default = award notices). 313k notices in full — the MDB buyer universe
85
+ (IBRD/IDA development projects in 100+ countries); supplier keys are extracted from the embedded notice_text
86
+ section (awarded_firms[]{name, supp_id} — firm name + WB supplier id), notice_text itself is not returned
87
+ (embedded contact addresses/phones stay out of the face)."""
88
+ self._pr_only("wb_procnotices")
89
+ r = wb_proc.procnotices_raw(notice_type, rows=rows, os_=os,
90
+ cache=self._c._cache, no_cache=no_cache if no_cache is not None else self._c.no_cache)
91
+ res = {k: v for k, v in r.items() if k != "_meta"}
92
+ prows = res["rows"]
93
+ n_firms = sum(len(x["awarded_firms"]) for x in prows)
94
+ if prows:
95
+ total_str = f"{res['total']:,}" if res["total"] is not None else "not echoed upstream"
96
+ reason = (f"World Bank procurement notices ({r['params']['notice_type']}): {total_str} notices in full, "
97
+ f"offset {os} returned {len(prows)} rows — this page extracted {n_firms} awarded suppliers "
98
+ f"(firm name + WB supplier id supp_id, cross-checkable against the WB debarred list on the same key)")
99
+ else:
100
+ reason = (f"World Bank procurement notices ({r['params']['notice_type']}): 0 rows — "
101
+ f"an out-of-range os or a notice_type with no such value is a legitimate answer (total={res['total']})")
102
+ ev = [{"quote": f"{x['id']} — {x['project_ctry_name']} — {x['project_id']} — "
103
+ f"{' / '.join(f['name'] + ' (' + f['supp_id'] + ')' for f in x['awarded_firms']) or '(no award section in notice)'}",
104
+ "url": "https://search.worldbank.org/api/v2/procnotices"}
105
+ for x in prows[:3] if x.get("id")]
106
+ return self._flat_env(tool="wb_procnotices",
107
+ input={"notice_type": notice_type, "rows": rows, "os": os},
108
+ reason=reason, result=res, evidence=ev, provider_id="wb_proc",
109
+ metas=[r["_meta"]],
110
+ query={"source": "raw", "upstream": r["params"]})
111
+
112
+ def ny_abo_contracts(self, where=None, *, limit=20, offset=0, order=None,
113
+ execution=None, no_cache=None):
114
+ """procurement ny_abo_contracts: New York State authority contracts (data.ny.gov Socrata ehig-g5x3;
115
+ where→$where raw SoQL verbatim channel, limit/offset/order mirrored by the same names). 276k rows corpus-wide —
116
+ authority_name buyer authority × vendor_name + full-address supplier (no UEI/DUNS, name walk),
117
+ four value columns + the MWBE/NYS procurement-preference flag family."""
118
+ self._pr_only("ny_abo_contracts")
119
+ r = ny_open_data.abo_contracts_raw(where, limit=limit, offset=offset, order=order,
120
+ cache=self._c._cache, no_cache=no_cache if no_cache is not None else self._c.no_cache)
121
+ res = {k: v for k, v in r.items() if k != "_meta"}
122
+ rows = res["rows"]
123
+ if rows:
124
+ reason = (f"NY State authority contracts (ABO): returned {len(rows)} rows (from offset={offset}; Socrata sends no total echo)"
125
+ f" — buyer authority_name × supplier vendor_name + full address (name+address walk)"
126
+ f" (sample {rows[0].get('authority_name') or '—'} × {rows[0].get('vendor_name') or '—'})")
127
+ else:
128
+ reason = "NY ABO: 0 rows — either the where filter has no match or offset is past the end; both are legitimate answers (not errors)"
129
+ ev = [{"quote": f"{x.get('authority_name') or '—'} × {x.get('vendor_name') or '—'} — "
130
+ f"{x.get('contract_amount') or '—'} USD (awarded {x.get('award_date') or '—'})",
131
+ "url": "https://data.ny.gov/resource/ehig-g5x3.json"}
132
+ for x in rows[:3] if x.get("vendor_name")]
133
+ return self._flat_env(tool="ny_abo_contracts",
134
+ input={"where": where, "limit": limit, "offset": offset, "order": order},
135
+ reason=reason, result=res, evidence=ev, provider_id="ny_open_data",
136
+ metas=[r["_meta"]],
137
+ query={"source": "raw", "upstream": r["params"]})
138
+
139
+ def ny_mta_procurements(self, where=None, *, limit=20, offset=0, order=None,
140
+ execution=None, no_cache=None):
141
+ """procurement ny_mta_procurements: New York MTA procurements (data.ny.gov Socrata twsw-2mqa;
142
+ where→$where raw SoQL verbatim channel, limit/offset/order mirrored by the same names). 107k rows corpus-wide —
143
+ buyer is always the MTA, vendor_name supplier + value family + vendor_is_nys_or_fbe preference flag."""
144
+ self._pr_only("ny_mta_procurements")
145
+ r = ny_open_data.mta_procurements_raw(where, limit=limit, offset=offset, order=order,
146
+ cache=self._c._cache, no_cache=no_cache if no_cache is not None else self._c.no_cache)
147
+ res = {k: v for k, v in r.items() if k != "_meta"}
148
+ rows = res["rows"]
149
+ if rows:
150
+ reason = (f"NY MTA procurements: returned {len(rows)} rows (from offset={offset}; Socrata sends no total echo)"
151
+ f" — supplier vendor_name + full address (buyer is always the MTA)"
152
+ f" (sample {rows[0].get('vendor_name') or '—'} — {rows[0].get('contract_amount') or '—'} USD)")
153
+ else:
154
+ reason = "NY MTA: 0 rows — either the where filter has no match or offset is past the end; both are legitimate answers (not errors)"
155
+ ev = [{"quote": f"MTA × {x.get('vendor_name') or '—'} — {x.get('contract_amount') or '—'} USD"
156
+ f" (awarded {x.get('award_date') or '—'}, {x.get('status') or '—'})",
157
+ "url": "https://data.ny.gov/resource/twsw-2mqa.json"}
158
+ for x in rows[:3] if x.get("vendor_name")]
159
+ return self._flat_env(tool="ny_mta_procurements",
160
+ input={"where": where, "limit": limit, "offset": offset, "order": order},
161
+ reason=reason, result=res, evidence=ev, provider_id="ny_open_data",
162
+ metas=[r["_meta"]],
163
+ query={"source": "raw", "upstream": r["params"]})
164
+
165
+ def ungm_contract_awards(self, supplier=None, *, title=None, reference=None,
166
+ award_from=None, award_to=None, countries=None, agencies=None,
167
+ unspscs=None, page_index=1, page_size=15, sort_field="AwardDate",
168
+ sort_ascending=False, execution=None, no_cache=None):
169
+ """procurement ungm_contract_awards: UN Global Marketplace contract-award search
170
+ (www.ungm.org/Public/ContractAward PublicSearch; Supplier/Title/Reference/AwardFrom/
171
+ AwardTo/Countries/Agencies/UNSPSCs under their upstream names, page_index/page_size
172
+ paging). Full corpus ~50k awards — the UN-agency buyer universe (UNDP/UNFPA/ILO/
173
+ UNOPS…) × global suppliers (vendor name + supplier country); rows = id (the award
174
+ key) + six slots. The anti-forgery pairing session is reused for 15 minutes and
175
+ re-collected on a 400; the browser-UA gate is built into the provider layer."""
176
+ self._pr_only("ungm_contract_awards")
177
+ r = ungm.awards_raw(supplier, title=title, reference=reference, award_from=award_from,
178
+ award_to=award_to, countries=countries, agencies=agencies,
179
+ unspscs=unspscs, page_index=page_index, page_size=page_size,
180
+ sort_field=sort_field, sort_ascending=sort_ascending,
181
+ cache=self._c._cache,
182
+ no_cache=no_cache if no_cache is not None else self._c.no_cache)
183
+ res = {k: v for k, v in r.items() if k != "_meta"}
184
+ rows = res["rows"]
185
+ n_vendor = sum(1 for x in rows if x.get("vendor"))
186
+ if rows:
187
+ reason = (f"UNGM award search: {res['total']:,} total hits, page {page_index} returned {len(rows)} rows"
188
+ f" ({n_vendor} rows carry a vendor name) — UN-agency buyers × global suppliers"
189
+ + (f", supplier=\"{supplier}\"" if supplier else "")
190
+ + f" (sample {rows[0].get('agency') or '—'} × {rows[0].get('vendor') or '—'}, "
191
+ f"awarded {rows[0].get('award_date') or '—'})")
192
+ else:
193
+ reason = (f"UNGM award search: 0 rows — no filter match or a past-the-end page number, "
194
+ f"both legitimate answers (total={res['total']})")
195
+ ev = [{"quote": f"{x['id']} — {x.get('agency') or '—'} × {x.get('vendor') or '—'} — "
196
+ f"{x.get('supplier_country') or '—'} ({x.get('award_date') or '—'})",
197
+ "url": f"https://www.ungm.org/Public/ContractAward/{x['id']}"}
198
+ for x in rows[:3] if x.get("id")]
199
+ return self._flat_env(tool="ungm_contract_awards",
200
+ input={"supplier": supplier, "title": title, "reference": reference,
201
+ "award_from": award_from, "award_to": award_to,
202
+ "countries": countries, "agencies": agencies,
203
+ "unspscs": unspscs, "page_index": page_index,
204
+ "page_size": page_size, "sort_field": sort_field,
205
+ "sort_ascending": sort_ascending},
206
+ reason=reason, result=res, evidence=ev, provider_id="ungm",
207
+ metas=[r["_meta"]],
208
+ query={"source": "raw", "upstream": r["params"]})
209
+
210
+ def ny_ogs_payments(self, where=None, *, limit=20, offset=0, order=None,
211
+ execution=None, no_cache=None):
212
+ """procurement ny_ogs_payments: New York State OGS D&C payments (data.ny.gov Socrata
213
+ rb9h-9fit; where→$where the bare-SoQL upstream-name channel, limit/offset/order
214
+ mirrored under the same names). Full corpus ~69k rows — the buyer is always OGS,
215
+ rows = contractnumber×vendor×paymentamount + fiscalyear/quarter/county (no address
216
+ column; a name walk with a single key); pairs with the ABO/MTA faces to complete
217
+ the New York State three-buyer universe."""
218
+ self._pr_only("ny_ogs_payments")
219
+ r = ny_open_data.ogs_dc_payments_raw(where, limit=limit, offset=offset, order=order,
220
+ cache=self._c._cache,
221
+ no_cache=no_cache if no_cache is not None else self._c.no_cache)
222
+ res = {k: v for k, v in r.items() if k != "_meta"}
223
+ rows = res["rows"]
224
+ if rows:
225
+ reason = (f"NY OGS D&C payments: returned {len(rows)} rows (from offset={offset}; Socrata sends no total echo)"
226
+ f" — the buyer is always OGS, contractnumber×vendor×paymentamount three pillars"
227
+ f" (sample {rows[0].get('vendor') or '—'} — {rows[0].get('paymentamount') or '—'} USD, "
228
+ f"{rows[0].get('fiscalyear') or '—'} Q{rows[0].get('quarter') or '—'})")
229
+ else:
230
+ reason = "NY OGS: 0 rows — the where filter has no match or offset is past the end, both legitimate answers (not errors)"
231
+ ev = [{"quote": f"OGS × {x.get('vendor') or '—'} — {x.get('paymentamount') or '—'} USD"
232
+ f" ({x.get('contractnumber') or '—'}, {x.get('fiscalyear') or '—'} Q{x.get('quarter') or '—'})",
233
+ "url": "https://data.ny.gov/resource/rb9h-9fit.json"}
234
+ for x in rows[:3] if x.get("vendor")]
235
+ return self._flat_env(tool="ny_ogs_payments",
236
+ input={"where": where, "limit": limit, "offset": offset, "order": order},
237
+ reason=reason, result=res, evidence=ev, provider_id="ny_open_data",
238
+ metas=[r["_meta"]],
239
+ query={"source": "raw", "upstream": r["params"]})