mostlyright-data 0.31.1__tar.gz → 0.31.3__tar.gz

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 (136) hide show
  1. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/PKG-INFO +1 -1
  2. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/transport.py +227 -1
  3. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_public.py +17 -1
  4. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/.gitignore +0 -0
  5. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/README.md +0 -0
  6. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/pyproject.toml +0 -0
  7. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/scripts/hatch_build.py +0 -0
  8. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/SKILL.md +0 -0
  9. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/agents/openai.yaml +0 -0
  10. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  11. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  12. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  13. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  14. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  15. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  16. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  17. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  18. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  19. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/agent-protocol.md +0 -0
  20. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  21. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  22. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/boundaries.md +0 -0
  23. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  24. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/commands.md +0 -0
  25. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  26. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
  27. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/installation-parity.md +0 -0
  28. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/live-run.md +0 -0
  29. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  30. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  31. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  32. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/one-install.md +0 -0
  33. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/patterns.md +0 -0
  34. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/prediction-labels.md +0 -0
  35. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  36. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/promote.md +0 -0
  37. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/readers.md +0 -0
  38. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/receipts.md +0 -0
  39. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/recipe-test.md +0 -0
  40. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  41. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  42. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/reference-pages.md +0 -0
  43. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/required-protocol.md +0 -0
  44. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/source-credentials.md +0 -0
  45. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/sources.md +0 -0
  46. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  47. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/transforms.md +0 -0
  48. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  49. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  50. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  51. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/__init__.py +0 -0
  52. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  53. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/canonical.py +0 -0
  54. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/formats.py +0 -0
  55. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  56. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/key_seam.py +0 -0
  57. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/page_coverage.py +0 -0
  58. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  59. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/session_probes.py +0 -0
  60. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/skill_assets.py +0 -0
  61. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/table_manifest.py +0 -0
  62. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  63. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  64. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  65. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/activity.py +0 -0
  66. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  67. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/backoff.py +0 -0
  68. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/categories.py +0 -0
  69. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/commands.py +0 -0
  70. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  71. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/download.py +0 -0
  72. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
  73. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  74. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
  75. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  76. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/parity.py +0 -0
  77. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/probe.py +0 -0
  78. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  79. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  80. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/propose.py +0 -0
  81. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  82. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  83. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  84. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  85. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  86. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
  87. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
  88. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/release_check.py +0 -0
  89. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/research.py +0 -0
  90. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/router.py +0 -0
  91. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  92. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
  93. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/runs.py +0 -0
  94. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/session.py +0 -0
  95. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/stream.py +0 -0
  96. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  97. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  98. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/support.py +0 -0
  99. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/table_search_contract.json +0 -0
  100. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  101. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4.py +0 -0
  102. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  103. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  104. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  105. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  106. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  107. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  108. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  109. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
  110. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  111. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  112. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  113. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  114. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  115. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
  116. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  117. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  118. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  119. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  120. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  121. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  122. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  123. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  124. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  125. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  126. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  127. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  128. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  129. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  130. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  131. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  132. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/login.py +0 -0
  133. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  134. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  135. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  136. {mostlyright_data-0.31.1 → mostlyright_data-0.31.3}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.31.1
3
+ Version: 0.31.3
4
4
  Summary: Mostly Right hosted CLI for reviewed datasets
5
5
  Project-URL: Homepage, https://mostlyright.md/
6
6
  Project-URL: Documentation, https://mostlyright.md/docs/guides/cli/
@@ -10,10 +10,20 @@ to relax any of it.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import hashlib
14
+ import http.client
13
15
  import ipaddress
16
+ import math
17
+ import re
14
18
  import ssl
19
+ import threading
20
+ import time
15
21
  from collections.abc import Callable, Iterator, Mapping
16
22
  from contextlib import contextmanager
23
+ from email.message import Message
24
+ from email.utils import parsedate_to_datetime
25
+ from http.cookiejar import Cookie, CookieJar, DefaultCookiePolicy
26
+ from http.cookies import CookieError, SimpleCookie
17
27
  from typing import Any, Protocol
18
28
  from urllib.error import HTTPError
19
29
  from urllib.parse import urlsplit
@@ -119,8 +129,9 @@ class UrlLibTransport:
119
129
 
120
130
  def __init__(self, *, timeout: float | Callable[[], float] = REQUEST_TIMEOUT_SECONDS) -> None:
121
131
  self._timeout = timeout if callable(timeout) else float(timeout)
132
+ self._context = ssl.create_default_context()
122
133
  self._opener = build_opener(
123
- ProxyHandler({}), HTTPSHandler(context=ssl.create_default_context()), _NoRedirect()
134
+ ProxyHandler({}), HTTPSHandler(context=self._context), _NoRedirect()
124
135
  )
125
136
 
126
137
  def _clock(self) -> float:
@@ -171,6 +182,221 @@ class UrlLibTransport:
171
182
  response.close()
172
183
 
173
184
 
185
+ class _RoutingCookiePolicy(DefaultCookiePolicy):
186
+ """Only Cloud Run's observed host-only routing cookie is retained."""
187
+
188
+ def set_ok(self, cookie: Cookie, request: Request) -> bool:
189
+ if (
190
+ cookie.name != "GAESA"
191
+ or cookie.domain_specified
192
+ or not cookie.path_specified
193
+ or cookie.path != "/"
194
+ or len(cookie.value or "") > 4096
195
+ ):
196
+ return False
197
+ # Cloud Run emits this without Secure. The transport accepts only
198
+ # verified HTTPS, and the stored form must never permit an HTTP send.
199
+ cookie.secure = True
200
+ return super().set_ok(cookie, request)
201
+
202
+
203
+ class _RoutingCookieResponse:
204
+ def __init__(self, header: str) -> None:
205
+ self.headers = Message()
206
+ self.headers["Set-Cookie"] = header
207
+
208
+ def info(self) -> Message:
209
+ return self.headers
210
+
211
+
212
+ def _remember_routing_cookie(
213
+ jar: CookieJar, response: http.client.HTTPResponse, request: Request
214
+ ) -> None:
215
+ candidates = [
216
+ value
217
+ for key, value in response.getheaders()
218
+ if key.lower() == "set-cookie" and value.partition("=")[0].strip() == "GAESA"
219
+ ]
220
+ if len(candidates) != 1:
221
+ return
222
+ header = candidates[0]
223
+ if (
224
+ len(header) > 8192
225
+ or "\r" in header
226
+ or "\n" in header
227
+ or len(re.findall(r"(?:^|[;,])\s*GAESA\s*=", header)) != 1
228
+ ):
229
+ return
230
+ parsed = SimpleCookie()
231
+ try:
232
+ parsed.load(header)
233
+ if set(parsed) != {"GAESA"}:
234
+ return
235
+ routing = parsed["GAESA"]
236
+ if not re.fullmatch(r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*", routing.value):
237
+ return
238
+ if routing["max-age"]:
239
+ age = routing["max-age"]
240
+ if (
241
+ not re.fullmatch(r"-?\d+", age)
242
+ or len(age.lstrip("-")) > 10
243
+ or abs(int(age)) > 2_147_483_647
244
+ ):
245
+ return
246
+ if routing["expires"]:
247
+ parsedate_to_datetime(routing["expires"])
248
+ except (CookieError, ValueError, OverflowError):
249
+ return
250
+ try:
251
+ jar.extract_cookies(_RoutingCookieResponse(header), request)
252
+ except (ValueError, OverflowError):
253
+ # Optional routing metadata must never invalidate a complete answer.
254
+ return
255
+
256
+
257
+ class ReusableHttpsTransport(UrlLibTransport):
258
+ """One serialized, verified HTTPS connection for repeated query requests.
259
+
260
+ urllib deliberately sends Connection: close. A query client reuses its
261
+ own connection instead, never a global credential-bearing pool. Changing
262
+ origin closes the old socket. Responses are bounded and fully framed;
263
+ any failure discards the socket and is raised without replaying a POST.
264
+ Only the host-only root GAESA routing cookie is retained in memory, bound
265
+ to this client's exact origin and authorization context, never login cookies.
266
+ Streams retain the original urllib/proxy-free/redirect-free behavior.
267
+ """
268
+
269
+ def __init__(self, *, timeout: float | Callable[[], float] = REQUEST_TIMEOUT_SECONDS) -> None:
270
+ super().__init__(timeout=timeout)
271
+ self._lock = threading.Lock()
272
+ self._connection: http.client.HTTPSConnection | None = None
273
+ self._connection_origin: tuple[str, str, int] | None = None
274
+ self._routing = CookieJar(policy=_RoutingCookiePolicy())
275
+ self._routing_context: tuple[tuple[str, str, int], bytes] | None = None
276
+
277
+ def _discard(self) -> None:
278
+ connection, self._connection = self._connection, None
279
+ self._connection_origin = None
280
+ if connection is not None:
281
+ connection.close()
282
+
283
+ def close(self) -> None:
284
+ with self._lock:
285
+ self._discard()
286
+ self._routing.clear()
287
+ self._routing_context = None
288
+
289
+ def __enter__(self) -> ReusableHttpsTransport:
290
+ return self
291
+
292
+ def __exit__(self, *_args: Any) -> None:
293
+ self.close()
294
+
295
+ def __del__(self) -> None:
296
+ connection = getattr(self, "_connection", None)
297
+ if connection is not None:
298
+ connection.close()
299
+
300
+ def request(
301
+ self, method: str, url: str, headers: Mapping[str, str], body: bytes | None, maximum: int
302
+ ) -> tuple[int, bytes, Mapping[str, str]]:
303
+ if type(maximum) is not int or maximum < 0:
304
+ raise ThinLaneError("THIN_REQUEST_INVALID", "the response ceiling must be nonnegative")
305
+ parts = urlsplit(url)
306
+ service_origin(
307
+ f"{parts.scheme}://{parts.netloc}", "request origin", allow_loopback_http=False
308
+ )
309
+ if parts.scheme != "https" or parts.fragment or parts.hostname is None:
310
+ raise ThinLaneError(
311
+ "THIN_REQUEST_INVALID", "a reusable request requires a valid HTTPS URL"
312
+ )
313
+ origin = (parts.scheme, parts.hostname.lower(), parts.port or 443)
314
+ if any(
315
+ name.lower() == "host" and value.lower() != parts.netloc.lower()
316
+ for name, value in headers.items()
317
+ ):
318
+ raise ThinLaneError(
319
+ "THIN_REQUEST_INVALID", "the Host header differs from the request origin"
320
+ )
321
+ budget = self._clock()
322
+ if not math.isfinite(budget) or budget <= 0:
323
+ raise TimeoutError("the request deadline has elapsed")
324
+ deadline = time.monotonic() + budget
325
+ if not self._lock.acquire(timeout=budget):
326
+ raise TimeoutError("the request deadline elapsed waiting for its connection")
327
+
328
+ def remaining() -> float:
329
+ value = deadline - time.monotonic()
330
+ if value <= 0:
331
+ raise TimeoutError("the request deadline has elapsed")
332
+ return value
333
+
334
+ try:
335
+ context_headers = sorted(
336
+ (name.lower(), value)
337
+ for name, value in headers.items()
338
+ if name.lower() in {"authorization", "proxy-authorization", "cookie", "x-api-key"}
339
+ )
340
+ context = (origin, hashlib.sha256(repr(context_headers).encode()).digest())
341
+ if context != self._routing_context:
342
+ self._routing.clear()
343
+ self._routing_context = context
344
+ caller_cookie = any(name.lower() == "cookie" for name in headers)
345
+ cookie_request = Request(url, headers=dict(with_user_agent(headers)), method=method)
346
+ if not caller_cookie:
347
+ self._routing.add_cookie_header(cookie_request)
348
+ if self._connection_origin != origin:
349
+ self._discard()
350
+ if self._connection is None:
351
+ self._connection = http.client.HTTPSConnection(
352
+ origin[1], origin[2], timeout=remaining(), context=self._context
353
+ )
354
+ self._connection_origin = origin
355
+ connection = self._connection
356
+ connection.timeout = remaining()
357
+ if connection.sock is not None:
358
+ connection.sock.settimeout(remaining())
359
+ path = parts.path or "/"
360
+ if parts.query:
361
+ path += "?" + parts.query
362
+ connection.request(method, path, body=body, headers=dict(cookie_request.header_items()))
363
+ if connection.sock is not None:
364
+ connection.sock.settimeout(remaining())
365
+ response = connection.getresponse()
366
+ expected = response.length
367
+ chunks: list[bytes] = []
368
+ length = 0
369
+ while length <= maximum:
370
+ if connection.sock is not None:
371
+ connection.sock.settimeout(remaining())
372
+ chunk = response.read1(min(64 * 1024, maximum + 1 - length))
373
+ if not chunk:
374
+ break
375
+ chunks.append(chunk)
376
+ length += len(chunk)
377
+ raw = b"".join(chunks)
378
+ if length > maximum:
379
+ raise ThinLaneError(
380
+ "THIN_RESPONSE_TOO_LARGE",
381
+ "the response exceeded the byte ceiling for its route",
382
+ )
383
+ if expected is not None and length != expected:
384
+ raise http.client.IncompleteRead(raw, expected - length)
385
+ if not caller_cookie:
386
+ _remember_routing_cookie(self._routing, response, cookie_request)
387
+ status = int(response.status)
388
+ response_headers = {key.lower(): value for key, value in response.headers.items()}
389
+ response.close()
390
+ if response.will_close or connection.sock is None or status < 200:
391
+ self._discard()
392
+ return status, raw, response_headers
393
+ except BaseException:
394
+ self._discard()
395
+ raise
396
+ finally:
397
+ self._lock.release()
398
+
399
+
174
400
  def service_origin(value: str, label: str, *, allow_loopback_http: bool) -> str:
175
401
  """Return ``value`` when it is a bare HTTPS origin, and refuse it otherwise.
176
402
 
@@ -46,6 +46,7 @@ from mostlyright.data_harness.thin.session import (
46
46
  )
47
47
  from mostlyright.data_harness.thin.transport import (
48
48
  MAX_JSON_RESPONSE_BYTES,
49
+ ReusableHttpsTransport,
49
50
  ThinLaneError,
50
51
  Transport,
51
52
  UrlLibTransport,
@@ -896,10 +897,25 @@ class PublicQueryClient(CloudClient):
896
897
  def __init__(self, credentials, *, transport=None) -> None:
897
898
  # Let Cloud's gateway deadline return its own refusal and request id first.
898
899
  super().__init__(credentials, transport=transport)
900
+ self._owned_query_transport = (
901
+ ReusableHttpsTransport(timeout=150.0) if transport is None else None
902
+ )
899
903
  self._query_client = CloudClient(
900
- credentials, transport=transport or UrlLibTransport(timeout=150.0)
904
+ credentials,
905
+ transport=transport if transport is not None else self._owned_query_transport,
901
906
  )
902
907
 
908
+ def close(self) -> None:
909
+ """Close this client's owned query connection; injected transports stay caller-owned."""
910
+ if self._owned_query_transport is not None:
911
+ self._owned_query_transport.close()
912
+
913
+ def __enter__(self) -> PublicQueryClient:
914
+ return self
915
+
916
+ def __exit__(self, *_args: Any) -> None:
917
+ self.close()
918
+
903
919
  def query_table(self, table_id: str, body: Mapping[str, Any]) -> dict[str, Any]:
904
920
  answer = self._query_client.call(
905
921
  "POST",