mostlyright-data 0.31.12__tar.gz → 0.31.13__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 (140) hide show
  1. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/PKG-INFO +8 -1
  2. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/README.md +7 -0
  3. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/router.py +2 -0
  4. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/runs.py +2 -0
  5. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/session.py +3 -1
  6. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/stream.py +2 -0
  7. mostlyright_data-0.31.13/src/mostlyright/data_harness/thin/user_agent.py +234 -0
  8. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_public.py +3 -1
  9. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_query.py +2 -0
  10. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/attendance.py +11 -6
  11. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/cloud_auth.py +2 -2
  12. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/clarify.py +10 -5
  13. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/login.py +7 -2
  14. mostlyright_data-0.31.12/src/mostlyright/data_harness/thin/user_agent.py +0 -76
  15. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/.gitignore +0 -0
  16. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/pyproject.toml +0 -0
  17. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/scripts/hatch_build.py +0 -0
  18. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/SKILL.md +0 -0
  19. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/agents/openai.yaml +0 -0
  20. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/1-open-the-page-and-the-link-to-it-in-the-first-message.md +0 -0
  21. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/2-brief-two-to-four-questions-each-with-a-recommended-answer.md +0 -0
  22. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/3-probe-read-a-source-before-committing-to-it.md +0 -0
  23. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/4-decide-say-what-you-chose-what-you-refused-and-ask-one-question.md +0 -0
  24. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/5-draft-one-recipe-document-one-call.md +0 -0
  25. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/6-build-one-run-sized-to-acquire-every-measured-source-whole.md +0 -0
  26. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/7-interrogate-ask-the-run-what-it-actually-delivered.md +0 -0
  27. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/8-fix-revise-the-document-and-register-it-again.md +0 -0
  28. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/9-present-only-what-survived-inspection-with-caveats.md +0 -0
  29. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/agent-protocol.md +0 -0
  30. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/autonomous-delivery.md +0 -0
  31. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/before-the-first-tool-call.md +0 -0
  32. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/boundaries.md +0 -0
  33. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/cloud-authentication-preflight.md +0 -0
  34. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/commands.md +0 -0
  35. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/cross-repository-protocol-reference.md +0 -0
  36. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/enriching-with-small-models.md +0 -0
  37. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/installation-parity.md +0 -0
  38. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/live-run.md +0 -0
  39. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/making-a-table-incremental.md +0 -0
  40. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/narrating-the-run.md +0 -0
  41. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/not-hosted-yet.md +0 -0
  42. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/one-install.md +0 -0
  43. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/patterns.md +0 -0
  44. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/prediction-labels.md +0 -0
  45. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/preparing-table-search.md +0 -0
  46. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/promote.md +0 -0
  47. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/readers.md +0 -0
  48. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/receipts.md +0 -0
  49. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recipe-test.md +0 -0
  50. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recording-a-stream-venue.md +0 -0
  51. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/recovering-an-import-failure.md +0 -0
  52. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/reference-pages.md +0 -0
  53. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/required-protocol.md +0 -0
  54. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/source-credentials.md +0 -0
  55. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/sources.md +0 -0
  56. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/the-one-thing-to-say-about-the-skill-itself.md +0 -0
  57. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/transforms.md +0 -0
  58. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/user-communication-contract.md +0 -0
  59. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/references/writing-a-decision-record.md +0 -0
  60. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  61. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/__init__.py +0 -0
  62. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/agent_protocol.py +0 -0
  63. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/canonical.py +0 -0
  64. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/formats.py +0 -0
  65. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  66. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/key_seam.py +0 -0
  67. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/page_coverage.py +0 -0
  68. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  69. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/session_probes.py +0 -0
  70. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/skill_assets.py +0 -0
  71. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/table_manifest.py +0 -0
  72. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/__init__.py +0 -0
  73. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  74. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  75. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/activity.py +0 -0
  76. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  77. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/backoff.py +0 -0
  78. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/categories.py +0 -0
  79. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/commands.py +0 -0
  80. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  81. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/delivery.py +0 -0
  82. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/description_shape.py +0 -0
  83. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/download.py +0 -0
  84. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/enrichment.py +0 -0
  85. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/fast_features.py +0 -0
  86. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/matching_benchmark.py +0 -0
  87. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  88. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/parity.py +0 -0
  89. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/probe.py +0 -0
  90. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/progress_rate.py +0 -0
  91. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  92. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/propose.py +0 -0
  93. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  94. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_brief.py +0 -0
  95. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_explain.py +0 -0
  96. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_forecast.py +0 -0
  97. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_lint.py +0 -0
  98. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/recipe_test.py +0 -0
  99. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/refresh_rehearsal.py +0 -0
  100. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/release_check.py +0 -0
  101. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/research.py +0 -0
  102. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/run_diagnose.py +0 -0
  103. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/run_prefix.py +0 -0
  104. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  105. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/studio_capabilities.py +0 -0
  106. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/support.py +0 -0
  107. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/table_search_contract.json +0 -0
  108. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/table_view.py +0 -0
  109. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/transport.py +0 -0
  110. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4.py +0 -0
  111. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  112. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  113. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_connected.py +0 -0
  114. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  115. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  116. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  117. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  118. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_matching.py +0 -0
  119. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  120. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_reader.py +0 -0
  121. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  122. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_search.py +0 -0
  123. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_search_contract.py +0 -0
  124. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  125. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  126. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  127. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  128. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  129. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  130. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  131. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  132. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  133. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  134. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  135. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credential_store.py +0 -0
  136. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/credentials.py +0 -0
  137. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  138. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  139. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/src/mostlyright/data_harness/ux/remediation.py +0 -0
  140. {mostlyright_data-0.31.12 → mostlyright_data-0.31.13}/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.12
3
+ Version: 0.31.13
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/
@@ -46,6 +46,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
46
46
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
47
47
  switches that off.
48
48
 
49
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
50
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
51
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
52
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
53
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
54
+ switches both off.
55
+
49
56
  ## Sign in
50
57
 
51
58
  ```bash
@@ -31,6 +31,13 @@ Once a day, after a command has printed its answer, `mr-data` asks the Python pa
31
31
  a newer release is published and says so in one line on stderr. `MOSTLYRIGHT_RELEASE_CHECK=0`
32
32
  switches that off.
33
33
 
34
+ Every call to Mostly Right names the coding agent running `mr-data` in `X-Mostlyright-Agent`
35
+ (`claude-code`, `codex`, `gemini-cli`, `cursor`, `ci` or `none`, read from the variables those
36
+ hosts set), and a program that imports this package sends it too. An `mr-data` command also sends
37
+ one random id in `X-Mostlyright-Invocation`, so the requests of one command can be grouped; a
38
+ library call sends none. Neither is sent to storage hosts. `MOSTLYRIGHT_NO_AGENT_HEADERS=1`
39
+ switches both off.
40
+
34
41
  ## Sign in
35
42
 
36
43
  ```bash
@@ -52,6 +52,7 @@ from collections.abc import Callable, Mapping, Sequence
52
52
  from typing import Any
53
53
 
54
54
  from mostlyright.data_harness import skill_assets
55
+ from mostlyright.data_harness.thin import user_agent
55
56
  from mostlyright.data_harness.thin.transport import ThinLaneError
56
57
  from mostlyright.data_harness.thin.vocabulary import (
57
58
  CLASSIFICATION,
@@ -781,6 +782,7 @@ def main(argv: Sequence[str] | None = None) -> int:
781
782
  # not write, it never raises, and `MOSTLYRIGHT_SKILL_AUTOINSTALL=0` turns it off. A skill that
782
783
  # arrived only when somebody knew to ask for it helped exactly the people who did not need it.
783
784
  skill_assets.ensure_installed()
785
+ user_agent.begin_invocation()
784
786
 
785
787
  arguments = list(sys.argv[1:] if argv is None else argv)
786
788
  hosted_requested = _hosted_flag_present(arguments)
@@ -46,6 +46,7 @@ from mostlyright.data_harness.thin.transport import (
46
46
  UrlLibTransport,
47
47
  signed_transfer_url,
48
48
  )
49
+ from mostlyright.data_harness.thin.user_agent import agent_headers
49
50
 
50
51
  #: The contract version every payload below is written against. ``studio_boundary`` pins the
51
52
  #: generated client to the same 3.0.0 and to one OpenAPI source digest; this constant is the thin
@@ -279,6 +280,7 @@ class StudioApiClient:
279
280
  headers = {
280
281
  "Accept": "application/json",
281
282
  CLIENT_HEADER: CLIENT_NAME,
283
+ **agent_headers(),
282
284
  }
283
285
  if extra_headers:
284
286
  headers.update(extra_headers)
@@ -42,6 +42,7 @@ from mostlyright.data_harness.thin.transport import (
42
42
  UrlLibTransport,
43
43
  service_origin,
44
44
  )
45
+ from mostlyright.data_harness.thin.user_agent import agent_headers
45
46
  from mostlyright.data_harness.ux.credentials import ResolvedCloudCredentials
46
47
 
47
48
  #: Cloud's token-exchange route. Held identical to ``hosted_deploy.CLOUD_TOKEN_PATH`` by a test.
@@ -396,7 +397,7 @@ def open_studio_session(
396
397
  status, raw, headers = selected.request(
397
398
  "POST",
398
399
  f"{cloud_url}{CLOUD_TOKEN_PATH}",
399
- {"Accept": "application/json", "x-api-key": credentials.raw_key},
400
+ {"Accept": "application/json", "x-api-key": credentials.raw_key, **agent_headers()},
400
401
  b"",
401
402
  MAX_TOKEN_RESPONSE_BYTES,
402
403
  )
@@ -953,6 +954,7 @@ class CloudClient:
953
954
  headers = {
954
955
  "Accept": "application/json",
955
956
  "Authorization": f"Bearer {self._credentials.raw_key}",
957
+ **agent_headers(),
956
958
  }
957
959
  encoded: bytes | None = None
958
960
  if body is not None:
@@ -49,6 +49,7 @@ from mostlyright.data_harness.thin.session import (
49
49
  stated_sentence,
50
50
  )
51
51
  from mostlyright.data_harness.thin.transport import ThinLaneError, Transport, UrlLibTransport
52
+ from mostlyright.data_harness.thin.user_agent import agent_headers
52
53
 
53
54
  #: Upstream's frame names. The first two are the tick/proof partition; ``end`` is its
54
55
  #: end-of-stream notice.
@@ -593,6 +594,7 @@ class RunEventStream:
593
594
  headers = {
594
595
  "Accept": "text/event-stream",
595
596
  CLIENT_HEADER: CLIENT_NAME,
597
+ **agent_headers(),
596
598
  **session.authorization(),
597
599
  }
598
600
  # Resume in Studio's own cursor space when we have one. `from_seq` is only ever the
@@ -0,0 +1,234 @@
1
+ """The identity every first-party request to Mostly Right states.
2
+
3
+ Without it the standard library sends ``Python-urllib/3.x``, which is what every unattended
4
+ script on the internet sends: the CLI's own traffic could not be told apart from anything else in
5
+ a server log, so nothing downstream could measure it, rate-limit it separately, or exempt it.
6
+
7
+ The name follows the convention the two crawler lanes already use -- a product token, a version,
8
+ and the contact segment that says where to reach the people running it -- so an origin and our own
9
+ logs see one family of identities from this deployment rather than one per code path.
10
+
11
+ Two more headers ride beside it on first-party calls only (:func:`agent_headers`): which coding
12
+ agent, if any, is driving this process, and, under the ``mr-data`` command, one id shared by every
13
+ request that command makes. They go where ``X-Mostlyright-Client`` goes and on Cloud's own
14
+ routes, never through the transport, for the reason ``thin.runs.CLIENT_HEADER`` gives: the
15
+ transport also carries signed transfers to storage nobody here owns.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ import secrets
22
+ from collections.abc import Mapping
23
+ from functools import cache
24
+ from importlib.metadata import PackageNotFoundError, version
25
+ from typing import NamedTuple
26
+
27
+ #: The contact segment ``hosted_worker.CRAWLER_CONTACT_SEGMENT`` states for the keyed-source
28
+ #: agents and ``hosted_crawler_fetch.PUBLIC_HTTPS_USER_AGENT`` carries for public sources, with
29
+ #: the measurement that makes it load-bearing recorded at the first of those.
30
+ #:
31
+ #: It is spelled again here rather than imported because both of those modules import ``pyarrow``
32
+ #: at module scope and this package installs with no compiled extension at all -- and they are
33
+ #: Studio's now, so importing them is not a choice this repository has. The spellings are held
34
+ #: against each other by ``tests/test_thin_user_agent.py`` under
35
+ #: ``make verify-cross-repo STUDIO_ROOT=...``, which reads the other one out of Studio's source.
36
+ CRAWLER_CONTACT_SEGMENT = "(+https://mostlyright.md/crawler; crawler@mostlyright.md)"
37
+
38
+ #: The distribution this client is installed as, and the product token it therefore reports.
39
+ DISTRIBUTION_NAME = "mostlyright-data"
40
+
41
+ #: The version reported from a source tree, where no distribution metadata exists to read. A
42
+ #: number rather than a word, so every value this header can carry parses the same way.
43
+ UNINSTALLED_VERSION = "0.0.0"
44
+
45
+ _USER_AGENT_HEADER = "User-Agent"
46
+
47
+ #: The header naming the agent driving this process, one of :data:`AGENTS`.
48
+ AGENT_HEADER = "X-Mostlyright-Agent"
49
+
50
+ #: The header carrying :func:`invocation_id`, sent only after :func:`begin_invocation`.
51
+ INVOCATION_HEADER = "X-Mostlyright-Invocation"
52
+
53
+ #: Set to anything but ``0`` or empty, and neither header above is sent.
54
+ NO_AGENT_HEADERS_ENV = "MOSTLYRIGHT_NO_AGENT_HEADERS"
55
+
56
+ AGENT_CLAUDE_CODE = "claude-code"
57
+ AGENT_CODEX = "codex"
58
+ AGENT_GEMINI_CLI = "gemini-cli"
59
+ AGENT_CURSOR = "cursor"
60
+ AGENT_CI = "ci"
61
+ AGENT_NONE = "none"
62
+
63
+
64
+ class AgentMarker(NamedTuple):
65
+ """One variable an agent host exports into the commands it runs.
66
+
67
+ ``conversation`` says whether the attendance check (``ux.attendance``) reads the variable as
68
+ a conversation existing. Only a marker every headless run of that host lacks may say so:
69
+ ``CODEX_SANDBOX`` is set by ``codex exec`` on a cron job too, so it names Codex here and is
70
+ not read as a person being there.
71
+ """
72
+
73
+ agent: str
74
+ variable: str
75
+ conversation: bool
76
+
77
+
78
+ #: Every marker, grouped by agent in precedence order.
79
+ #:
80
+ #: Presence of a non-empty value is the signal and the value is never read: these are the hosts'
81
+ #: variables, and their values are theirs to change. A coding agent outranks ``ci``, so
82
+ #: ``claude -p`` inside a GitHub Action reports ``claude-code``. Nothing that every terminal of an
83
+ #: editor exports is listed, because a person typing there would read as the agent.
84
+ AGENT_MARKERS: tuple[AgentMarker, ...] = (
85
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDECODE", True),
86
+ AgentMarker(AGENT_CLAUDE_CODE, "CLAUDE_CODE_SESSION", True),
87
+ AgentMarker(AGENT_CODEX, "CODEX_SESSION", True),
88
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX", False),
89
+ AgentMarker(AGENT_CODEX, "CODEX_SANDBOX_NETWORK_DISABLED", False),
90
+ AgentMarker(AGENT_GEMINI_CLI, "GEMINI_CLI", False),
91
+ AgentMarker(AGENT_CURSOR, "CURSOR_AGENT", False),
92
+ AgentMarker(AGENT_CI, "CI", False),
93
+ AgentMarker(AGENT_CI, "GITHUB_ACTIONS", False),
94
+ )
95
+
96
+ #: Every value :data:`AGENT_HEADER` can carry.
97
+ AGENTS: tuple[str, ...] = (*dict.fromkeys(marker.agent for marker in AGENT_MARKERS), AGENT_NONE)
98
+
99
+ #: What :func:`invocation_id` returns: 16 random bytes as 32 lowercase hex digits.
100
+ INVOCATION_ID_PATTERN = r"^[0-9a-f]{32}$"
101
+
102
+ _invocation: str | None = None
103
+
104
+
105
+ @cache
106
+ def client_user_agent() -> str:
107
+ """Return the ``User-Agent`` this client presents to Mostly Right.
108
+
109
+ Cached because the answer is a property of the installed environment, and reading distribution
110
+ metadata walks ``sys.path``; a command that makes twenty calls would otherwise pay for it
111
+ twenty times.
112
+ """
113
+
114
+ try:
115
+ installed = version(DISTRIBUTION_NAME)
116
+ except PackageNotFoundError:
117
+ installed = UNINSTALLED_VERSION
118
+ return f"{DISTRIBUTION_NAME}/{installed} {CRAWLER_CONTACT_SEGMENT}"
119
+
120
+
121
+ def with_user_agent(headers: Mapping[str, str]) -> dict[str, str]:
122
+ """Return ``headers`` as a new dict carrying this client's identity.
123
+
124
+ A caller that stated its own ``User-Agent`` keeps it, under whatever spelling it used: HTTP
125
+ header names are case-insensitive, so adding a second one under a different capitalization
126
+ would send two identities rather than override one.
127
+ """
128
+
129
+ resolved = dict(headers)
130
+ if any(name.lower() == "user-agent" for name in resolved):
131
+ return resolved
132
+ resolved[_USER_AGENT_HEADER] = client_user_agent()
133
+ return resolved
134
+
135
+
136
+ def agent_env(agent: str) -> tuple[str, ...]:
137
+ """Return the variables that name ``agent``; empty for ``none`` and for a name not listed."""
138
+
139
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.agent == agent)
140
+
141
+
142
+ def conversation_env() -> tuple[str, ...]:
143
+ """Return the markers the attendance check reads as a conversation existing."""
144
+
145
+ return tuple(marker.variable for marker in AGENT_MARKERS if marker.conversation)
146
+
147
+
148
+ def first_set(names: tuple[str, ...], environ: Mapping[str, str] | None = None) -> str | None:
149
+ """Return the first of ``names`` set to a non-empty value, or ``None``."""
150
+
151
+ table = os.environ if environ is None else environ
152
+ return next((name for name in names if (table.get(name) or "").strip()), None)
153
+
154
+
155
+ def detect_agent(environ: Mapping[str, str] | None = None) -> str:
156
+ """Return the agent driving this process, one of :data:`AGENTS`."""
157
+
158
+ table = os.environ if environ is None else environ
159
+ for marker in AGENT_MARKERS:
160
+ if (table.get(marker.variable) or "").strip():
161
+ return marker.agent
162
+ return AGENT_NONE
163
+
164
+
165
+ def begin_invocation() -> str:
166
+ """Draw a fresh invocation id for the command starting now, and return it.
167
+
168
+ Called by the ``mr-data`` entry point once per command. A program importing this package
169
+ never calls it, so its requests carry no invocation id rather than one that lasts as long as
170
+ the process does.
171
+ """
172
+
173
+ global _invocation
174
+ _invocation = secrets.token_hex(16)
175
+ return _invocation
176
+
177
+
178
+ def end_invocation() -> None:
179
+ """Forget the current invocation id, so later requests carry none."""
180
+
181
+ global _invocation
182
+ _invocation = None
183
+
184
+
185
+ def invocation_id() -> str | None:
186
+ """Return the current command's id (see :data:`INVOCATION_ID_PATTERN`), or ``None``."""
187
+
188
+ return _invocation
189
+
190
+
191
+ def agent_headers(environ: Mapping[str, str] | None = None) -> dict[str, str]:
192
+ """Return the agent and invocation headers, or none when :data:`NO_AGENT_HEADERS_ENV` is set.
193
+
194
+ The invocation header is present only inside a command :func:`begin_invocation` started. A
195
+ first-party call adds these itself; the transport never does.
196
+ """
197
+
198
+ table = os.environ if environ is None else environ
199
+ if (table.get(NO_AGENT_HEADERS_ENV) or "").strip() not in ("", "0"):
200
+ return {}
201
+ headers = {AGENT_HEADER: detect_agent(table)}
202
+ if _invocation is not None:
203
+ headers[INVOCATION_HEADER] = _invocation
204
+ return headers
205
+
206
+
207
+ __all__ = [
208
+ "AGENTS",
209
+ "AGENT_CI",
210
+ "AGENT_CLAUDE_CODE",
211
+ "AGENT_CODEX",
212
+ "AGENT_CURSOR",
213
+ "AGENT_GEMINI_CLI",
214
+ "AGENT_HEADER",
215
+ "AGENT_MARKERS",
216
+ "AGENT_NONE",
217
+ "CRAWLER_CONTACT_SEGMENT",
218
+ "DISTRIBUTION_NAME",
219
+ "INVOCATION_HEADER",
220
+ "INVOCATION_ID_PATTERN",
221
+ "NO_AGENT_HEADERS_ENV",
222
+ "UNINSTALLED_VERSION",
223
+ "AgentMarker",
224
+ "agent_env",
225
+ "agent_headers",
226
+ "begin_invocation",
227
+ "client_user_agent",
228
+ "conversation_env",
229
+ "detect_agent",
230
+ "end_invocation",
231
+ "first_set",
232
+ "invocation_id",
233
+ "with_user_agent",
234
+ ]
@@ -52,7 +52,7 @@ from mostlyright.data_harness.thin.transport import (
52
52
  UrlLibTransport,
53
53
  service_origin,
54
54
  )
55
- from mostlyright.data_harness.thin.user_agent import with_user_agent
55
+ from mostlyright.data_harness.thin.user_agent import agent_headers, with_user_agent
56
56
  from mostlyright.data_harness.ux.credentials import (
57
57
  CLOUD_URL_ENV,
58
58
  DEFAULT_CLOUD_URL,
@@ -245,6 +245,7 @@ class PublicCatalogClient:
245
245
  {
246
246
  "Accept": accept,
247
247
  CLIENT_HEADER: CLIENT_NAME,
248
+ **agent_headers(),
248
249
  }
249
250
  )
250
251
 
@@ -960,6 +961,7 @@ class PublicQueryClient(CloudClient):
960
961
  headers = {
961
962
  "Accept": "application/json",
962
963
  "Authorization": f"Bearer {self._credentials.raw_key}",
964
+ **agent_headers(),
963
965
  }
964
966
  status, raw, response_headers = self._transport.request(
965
967
  "GET",
@@ -75,6 +75,7 @@ from mostlyright.data_harness.thin.transport import (
75
75
  Transport,
76
76
  UrlLibTransport,
77
77
  )
78
+ from mostlyright.data_harness.thin.user_agent import agent_headers
78
79
  from mostlyright.data_harness.thin.v4 import (
79
80
  GET_RUN_PATH,
80
81
  GET_RUN_QUERY_PATH,
@@ -329,6 +330,7 @@ class CloudRunQueryClient(CloudClient):
329
330
  headers = {
330
331
  "Accept": "application/json",
331
332
  "Authorization": f"Bearer {self._credentials.raw_key}",
333
+ **agent_headers(),
332
334
  }
333
335
  encoded: bytes | None = None
334
336
  if body is not None:
@@ -107,6 +107,13 @@ from collections.abc import Mapping, Sequence
107
107
  from dataclasses import dataclass
108
108
  from typing import Any
109
109
 
110
+ from mostlyright.data_harness.thin.user_agent import (
111
+ AGENT_CI,
112
+ agent_env,
113
+ conversation_env,
114
+ first_set,
115
+ )
116
+
110
117
  #: The variable the process that spawns ``mr-data`` sets to say whether the session it is driving
111
118
  #: has a person in it. Exactly ``"1"`` is attended and exactly ``"0"`` is unattended -- and the
112
119
  #: ``"0"`` is a veto no flag overrides, for the reason the module docstring gives. Every other value
@@ -156,7 +163,7 @@ _UNATTENDED_VALUE = "0"
156
163
  #: ⚠ AND ONLY A NON-EMPTY VALUE COUNTS. A shell that exports an empty variable has said nothing,
157
164
  #: and reading that as "a person is here" would make ``export CLAUDECODE=`` a way to turn the
158
165
  #: inference on by accident.
159
- AGENT_HOST_ENV: tuple[str, ...] = ("CLAUDECODE", "CLAUDE_CODE_SESSION", "CODEX_SESSION")
166
+ AGENT_HOST_ENV: tuple[str, ...] = conversation_env()
160
167
 
161
168
  #: The variables a headless runner sets, which take an agent-host marker back off the table.
162
169
  #:
@@ -170,7 +177,7 @@ AGENT_HOST_ENV: tuple[str, ...] = ("CLAUDECODE", "CLAUDE_CODE_SESSION", "CODEX_S
170
177
  #: Buildkite all export it; ``GITHUB_ACTIONS`` is named beside it because the scenario this closes
171
178
  #: is literally ``claude -p`` in a GitHub Action. Presence of a non-empty value is the whole test,
172
179
  #: for the reason the agent-host markers give: the values are somebody else's to change.
173
- HEADLESS_HOST_ENV: tuple[str, ...] = ("CI", "GITHUB_ACTIONS")
180
+ HEADLESS_HOST_ENV: tuple[str, ...] = agent_env(AGENT_CI)
174
181
 
175
182
 
176
183
  @dataclass(frozen=True)
@@ -264,11 +271,9 @@ def resolve_attendance(
264
271
  )
265
272
  # Step 5: the host named itself, which is a conversation existing -- unless a headless runner
266
273
  # named itself too, in which case the conversation is a print-mode one with nobody in it.
267
- named = next((name for name in AGENT_HOST_ENV if (table.get(name) or "").strip()), None)
274
+ named = first_set(AGENT_HOST_ENV, table)
268
275
  if named is not None:
269
- headless = next(
270
- (name for name in HEADLESS_HOST_ENV if (table.get(name) or "").strip()), None
271
- )
276
+ headless = first_set(HEADLESS_HOST_ENV, table)
272
277
  if headless is not None:
273
278
  return Attendance(
274
279
  attended=False,
@@ -16,7 +16,7 @@ from collections.abc import Callable, Mapping
16
16
  from dataclasses import dataclass
17
17
  from typing import Any, Final
18
18
 
19
- from mostlyright.data_harness.thin.user_agent import with_user_agent
19
+ from mostlyright.data_harness.thin.user_agent import agent_headers, with_user_agent
20
20
 
21
21
  REQUEST_TIMEOUT_SECONDS: Final = 10.0
22
22
  MAX_RESPONSE_BYTES: Final = 64 * 1024
@@ -241,7 +241,7 @@ class CloudAuthClient:
241
241
  def _request(self, method: str, path: str) -> CloudAuthResponse:
242
242
  request = urllib.request.Request(
243
243
  urllib.parse.urljoin(self._base_url + "/", path.lstrip("/")),
244
- headers=with_user_agent({"x-api-key": self._raw_key}),
244
+ headers=with_user_agent({"x-api-key": self._raw_key, **agent_headers()}),
245
245
  method=method,
246
246
  )
247
247
  try:
@@ -34,7 +34,12 @@ from collections.abc import Mapping
34
34
  from typing import Any
35
35
 
36
36
  from mostlyright.data_harness.thin.transport import ThinLaneError
37
- from mostlyright.data_harness.ux.attendance import ATTENDANCE_ENV, resolve_attendance
37
+ from mostlyright.data_harness.ux.attendance import (
38
+ AGENT_HOST_ENV,
39
+ ATTENDANCE_ENV,
40
+ HEADLESS_HOST_ENV,
41
+ resolve_attendance,
42
+ )
38
43
  from mostlyright.data_harness.ux.clarification import (
39
44
  CONTRACT_ALREADY_AGREED,
40
45
  checked_question,
@@ -118,10 +123,10 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
118
123
  help=(
119
124
  "state that a person is here to answer, which is the one reliable way to say so; an "
120
125
  "explicit refusal, by --unattended or MOSTLYRIGHT_ATTENDED=0, still wins. Without it, "
121
- "an agent host that names itself in the environment (CLAUDECODE, "
122
- "CLAUDE_CODE_SESSION, CODEX_SESSION) is read as a conversation with somebody in it -- "
123
- "but NOT when CI or GITHUB_ACTIONS is set beside it, because an agent host on a runner "
124
- "is headless. A cron job carries none of these and stays unattended"
126
+ "an agent host that names itself in the environment "
127
+ f"({', '.join(AGENT_HOST_ENV)}) is read as a conversation with somebody in it -- "
128
+ f"but NOT when {' or '.join(HEADLESS_HOST_ENV)} is set beside it, because an agent "
129
+ "host on a runner is headless. A cron job carries none of these and stays unattended"
125
130
  ),
126
131
  )
127
132
  presence.add_argument(
@@ -49,6 +49,7 @@ from urllib.request import (
49
49
 
50
50
  from mostlyright.data_harness.canonical import CanonicalJSONError, parse_foreign_json
51
51
  from mostlyright.data_harness.key_seam import LEGACY_API_KEY_ENV
52
+ from mostlyright.data_harness.thin.user_agent import agent_headers
52
53
  from mostlyright.data_harness.ux.cloud_auth import CloudAuthClient, CloudAuthError
53
54
  from mostlyright.data_harness.ux.credential_store import (
54
55
  CredentialEnvelope,
@@ -291,7 +292,11 @@ def _post_json(
291
292
  """
292
293
 
293
294
  body = json.dumps(dict(payload), sort_keys=True).encode("utf-8")
294
- request_headers = {"Content-Type": "application/json", "Accept": "application/json"}
295
+ request_headers = {
296
+ "Content-Type": "application/json",
297
+ "Accept": "application/json",
298
+ **agent_headers(),
299
+ }
295
300
  request_headers.update(headers or {})
296
301
  try:
297
302
  status, raw = transport.request("POST", url, request_headers, body, MAX_RESPONSE_BYTES)
@@ -324,7 +329,7 @@ def _get_json(
324
329
  caller reads a 404 as "this cloud does not offer the endpoint yet" rather than as a failure.
325
330
  """
326
331
 
327
- request_headers = {"Accept": "application/json"}
332
+ request_headers = {"Accept": "application/json", **agent_headers()}
328
333
  request_headers.update(headers or {})
329
334
  try:
330
335
  status, raw = transport.request("GET", url, request_headers, None, MAX_RESPONSE_BYTES)
@@ -1,76 +0,0 @@
1
- """The identity every first-party request to Mostly Right states.
2
-
3
- Without it the standard library sends ``Python-urllib/3.x``, which is what every unattended
4
- script on the internet sends: the CLI's own traffic could not be told apart from anything else in
5
- a server log, so nothing downstream could measure it, rate-limit it separately, or exempt it.
6
-
7
- The name follows the convention the two crawler lanes already use -- a product token, a version,
8
- and the contact segment that says where to reach the people running it -- so an origin and our own
9
- logs see one family of identities from this deployment rather than one per code path.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- from collections.abc import Mapping
15
- from functools import cache
16
- from importlib.metadata import PackageNotFoundError, version
17
-
18
- #: The contact segment ``hosted_worker.CRAWLER_CONTACT_SEGMENT`` states for the keyed-source
19
- #: agents and ``hosted_crawler_fetch.PUBLIC_HTTPS_USER_AGENT`` carries for public sources, with
20
- #: the measurement that makes it load-bearing recorded at the first of those.
21
- #:
22
- #: It is spelled again here rather than imported because both of those modules import ``pyarrow``
23
- #: at module scope and this package installs with no compiled extension at all -- and they are
24
- #: Studio's now, so importing them is not a choice this repository has. The spellings are held
25
- #: against each other by ``tests/test_thin_user_agent.py`` under
26
- #: ``make verify-cross-repo STUDIO_ROOT=...``, which reads the other one out of Studio's source.
27
- CRAWLER_CONTACT_SEGMENT = "(+https://mostlyright.md/crawler; crawler@mostlyright.md)"
28
-
29
- #: The distribution this client is installed as, and the product token it therefore reports.
30
- DISTRIBUTION_NAME = "mostlyright-data"
31
-
32
- #: The version reported from a source tree, where no distribution metadata exists to read. A
33
- #: number rather than a word, so every value this header can carry parses the same way.
34
- UNINSTALLED_VERSION = "0.0.0"
35
-
36
- _USER_AGENT_HEADER = "User-Agent"
37
-
38
-
39
- @cache
40
- def client_user_agent() -> str:
41
- """Return the ``User-Agent`` this client presents to Mostly Right.
42
-
43
- Cached because the answer is a property of the installed environment, and reading distribution
44
- metadata walks ``sys.path``; a command that makes twenty calls would otherwise pay for it
45
- twenty times.
46
- """
47
-
48
- try:
49
- installed = version(DISTRIBUTION_NAME)
50
- except PackageNotFoundError:
51
- installed = UNINSTALLED_VERSION
52
- return f"{DISTRIBUTION_NAME}/{installed} {CRAWLER_CONTACT_SEGMENT}"
53
-
54
-
55
- def with_user_agent(headers: Mapping[str, str]) -> dict[str, str]:
56
- """Return ``headers`` as a new dict carrying this client's identity.
57
-
58
- A caller that stated its own ``User-Agent`` keeps it, under whatever spelling it used: HTTP
59
- header names are case-insensitive, so adding a second one under a different capitalization
60
- would send two identities rather than override one.
61
- """
62
-
63
- resolved = dict(headers)
64
- if any(name.lower() == "user-agent" for name in resolved):
65
- return resolved
66
- resolved[_USER_AGENT_HEADER] = client_user_agent()
67
- return resolved
68
-
69
-
70
- __all__ = [
71
- "CRAWLER_CONTACT_SEGMENT",
72
- "DISTRIBUTION_NAME",
73
- "UNINSTALLED_VERSION",
74
- "client_user_agent",
75
- "with_user_agent",
76
- ]