mostlyright-data 0.19.8__tar.gz → 0.20.0__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 (72) hide show
  1. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/PKG-INFO +3 -3
  2. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/README.md +1 -1
  3. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/pyproject.toml +2 -2
  4. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/__init__.py +1 -1
  5. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/credential_store.py +43 -2
  6. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/credentials.py +11 -7
  7. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/login.py +2 -1
  8. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/remediation.py +14 -1
  9. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/.gitignore +0 -0
  10. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/scripts/hatch_build.py +0 -0
  11. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/skills/mr-data-build/SKILL.md +0 -0
  12. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/skills/mr-data-build/agents/openai.yaml +0 -0
  13. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/skills/mr-data-build/scripts/write_research_notebook.py +0 -0
  14. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/__init__.py +0 -0
  15. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/canonical.py +0 -0
  16. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/formats.py +0 -0
  17. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/hosted_crawler_protocol.py +0 -0
  18. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/key_seam.py +0 -0
  19. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/page_coverage.py +0 -0
  20. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/part_check_evidence.py +0 -0
  21. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/session_probes.py +0 -0
  22. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/skill_assets.py +0 -0
  23. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/table_manifest.py +0 -0
  24. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/acquire.py +0 -0
  25. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/acquire_cancel.py +0 -0
  26. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/activity.py +0 -0
  27. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/approvals.py +0 -0
  28. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/categories.py +0 -0
  29. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/commands.py +0 -0
  30. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/dataset-categories-v1.json +0 -0
  31. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/download.py +0 -0
  32. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/narrative.py +0 -0
  33. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/parity.py +0 -0
  34. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/probe.py +0 -0
  35. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/progress_vocabulary.py +0 -0
  36. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/propose.py +0 -0
  37. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/recipe.py +0 -0
  38. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/research.py +0 -0
  39. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/router.py +0 -0
  40. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/runs.py +0 -0
  41. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/session.py +0 -0
  42. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/stream.py +0 -0
  43. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/stream_venue.py +0 -0
  44. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/transport.py +0 -0
  45. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/user_agent.py +0 -0
  46. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4.py +0 -0
  47. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_artifacts.py +0 -0
  48. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_catalog.py +0 -0
  49. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_connections.py +0 -0
  50. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_dataset_covers.py +0 -0
  51. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_datasets.py +0 -0
  52. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_handoff.py +0 -0
  53. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_narrative.py +0 -0
  54. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_query.py +0 -0
  55. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_runs.py +0 -0
  56. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_secrets.py +0 -0
  57. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_stream.py +0 -0
  58. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/v4_tables.py +0 -0
  59. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/thin/vocabulary.py +0 -0
  60. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/__init__.py +0 -0
  61. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/attendance.py +0 -0
  62. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/clarification.py +0 -0
  63. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/cloud_auth.py +0 -0
  64. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/commands/__init__.py +0 -0
  65. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/commands/auth.py +0 -0
  66. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/commands/clarify.py +0 -0
  67. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/commands/login.py +0 -0
  68. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/commands/whoami.py +0 -0
  69. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/credential_native.py +0 -0
  70. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/path_kind.py +0 -0
  71. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/plain_file.py +0 -0
  72. {mostlyright_data-0.19.8 → mostlyright_data-0.20.0}/src/mostlyright/data_harness/ux/render.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mostlyright-data
3
- Version: 0.19.8
3
+ Version: 0.20.0
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/
7
- Requires-Python: >=3.13
7
+ Requires-Python: >=3.11
8
8
  Provides-Extra: build
9
9
  Requires-Dist: hatchling==1.28.0; extra == 'build'
10
10
  Requires-Dist: uv-build==0.11.3; extra == 'build'
@@ -22,7 +22,7 @@ they finish.
22
22
 
23
23
  ## Requirements
24
24
 
25
- Use CPython 3.13 or newer.
25
+ Use CPython 3.11 or newer.
26
26
 
27
27
  ## Install
28
28
 
@@ -10,7 +10,7 @@ they finish.
10
10
 
11
11
  ## Requirements
12
12
 
13
- Use CPython 3.13 or newer.
13
+ Use CPython 3.11 or newer.
14
14
 
15
15
  ## Install
16
16
 
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "mostlyright-data"
3
- version = "0.19.8"
3
+ version = "0.20.0"
4
4
  description = "Mostly Right hosted CLI for reviewed datasets"
5
5
  readme = "README.md"
6
- requires-python = ">=3.13"
6
+ requires-python = ">=3.11"
7
7
  # The published Harness package is a pure-Python, stdlib-only hosted client. Dataset execution,
8
8
  # native readers, and worker publication live in Mostly Right Studio.
9
9
  dependencies = []
@@ -1,6 +1,6 @@
1
1
  """The pure-Python hosted lane: auth, submit, stream, download, receipts.
2
2
 
3
- Every module in this package is importable on a bare CPython 3.13 with no compiled extension in
3
+ Every module in this package is importable on a bare CPython 3.11 with no compiled extension in
4
4
  site-packages. That is the whole contract of the package and it is asserted rather than intended:
5
5
  :mod:`tests.test_thin_profile` installs the base distribution into a fresh environment, refuses any
6
6
  ``.so``/``.pyd``/``.dylib`` it finds, and imports the command router there.
@@ -789,8 +789,13 @@ def _ensure_private_directory(path: Path) -> None:
789
789
  created = True
790
790
  except FileExistsError:
791
791
  created = False
792
- if created and os.name == "posix":
793
- os.chmod(path, 0o700, follow_symlinks=False)
792
+ if os.name == "posix":
793
+ if created:
794
+ # The umask may have stripped bits from the mode ``mkdir`` was asked for, and a
795
+ # directory this call just made is one it owns.
796
+ os.chmod(path, 0o700)
797
+ else:
798
+ _narrow_private_directory(path)
794
799
  _validate_private_directory(path)
795
800
  except OSError:
796
801
  raise CredentialStoreError(
@@ -798,6 +803,42 @@ def _ensure_private_directory(path: Path) -> None:
798
803
  ) from None
799
804
 
800
805
 
806
+ def _narrow_private_directory(path: Path) -> None:
807
+ """Bring a directory this user already owns down to 0700 before the gate below reads it.
808
+
809
+ Another tool can have created the same directory under an ordinary umask, which lands it at
810
+ 0755 and makes every later read and write refuse it. It is still this effective user's
811
+ directory, so narrowing it is repair rather than a decision.
812
+
813
+ The descriptor is the whole safety argument. ``O_NOFOLLOW`` and ``O_DIRECTORY`` mean a symlink
814
+ and a non-directory never open at all, so neither can be chmod'ed and both leave through the
815
+ ``OSError`` the caller turns into a refusal, and the owner is read off the same descriptor the
816
+ mode is written through, so a directory belonging to somebody else is left exactly as found for
817
+ :func:`_validate_private_directory` to refuse.
818
+ """
819
+
820
+ try:
821
+ descriptor = os.open(
822
+ path,
823
+ os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0),
824
+ )
825
+ except PermissionError:
826
+ # A directory without its owner-read bit cannot be opened, so the descriptor argument is
827
+ # unavailable and the kind and owner are read from the name instead.
828
+ info = os.lstat(path)
829
+ if stat.S_ISDIR(info.st_mode) and info.st_uid == os.geteuid():
830
+ os.chmod(path, 0o700)
831
+ return
832
+ try:
833
+ info = os.fstat(descriptor)
834
+ if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid():
835
+ return
836
+ if stat.S_IMODE(info.st_mode) != 0o700:
837
+ os.fchmod(descriptor, 0o700)
838
+ finally:
839
+ os.close(descriptor)
840
+
841
+
801
842
  def _validate_private_directory(path: Path) -> None:
802
843
  try:
803
844
  info = path.stat(follow_symlinks=False)
@@ -199,7 +199,7 @@ def store_credentials(
199
199
  except LoginError:
200
200
  raise
201
201
  except CredentialStoreError as error:
202
- raise _login_store_error(error) from None
202
+ raise login_store_error(error) from None
203
203
  except OSError:
204
204
  raise LoginError(
205
205
  "LOGIN_CREDENTIAL_MIGRATION_FAILED",
@@ -251,7 +251,7 @@ def read_credentials(
251
251
  except LoginError:
252
252
  raise
253
253
  except CredentialStoreError as error:
254
- raise _login_store_error(error) from None
254
+ raise login_store_error(error) from None
255
255
  except OSError:
256
256
  raise LoginError(
257
257
  "LOGIN_CREDENTIALS_UNREADABLE",
@@ -392,16 +392,19 @@ def _native_backend_name(directory: Path) -> str:
392
392
  try:
393
393
  return native_backend(directory).name
394
394
  except CredentialStoreError as error:
395
- raise _login_store_error(error) from None
395
+ raise login_store_error(error) from None
396
396
 
397
397
 
398
398
  def _backend_for(directory: Path, backend_name: str) -> CredentialBackend:
399
399
  if backend_name == "secure-file":
400
- return SecureFileBackend(directory, explicit=True)
400
+ try:
401
+ return SecureFileBackend(directory, explicit=True)
402
+ except CredentialStoreError as error:
403
+ raise login_store_error(error) from None
401
404
  try:
402
405
  backend = native_backend(directory)
403
406
  except CredentialStoreError as error:
404
- raise _login_store_error(error) from None
407
+ raise login_store_error(error) from None
405
408
  if backend.name != backend_name:
406
409
  raise LoginError(
407
410
  "LOGIN_CREDENTIAL_STORE_UNAVAILABLE",
@@ -588,7 +591,7 @@ def _install_manifest(path: Path, backend_name: str) -> None:
588
591
  stage.unlink()
589
592
 
590
593
 
591
- def _login_store_error(error: CredentialStoreError) -> LoginError:
594
+ def login_store_error(error: CredentialStoreError) -> LoginError:
592
595
  suffix = error.code.removeprefix("CREDENTIAL_")
593
596
  details = {
594
597
  "CREDENTIAL_STORE_UNAVAILABLE": (
@@ -606,7 +609,7 @@ def _login_store_error(error: CredentialStoreError) -> LoginError:
606
609
  }
607
610
  return LoginError(
608
611
  f"LOGIN_CREDENTIAL_{suffix}",
609
- details.get(error.code, "the protected credential operation failed"),
612
+ details.get(error.code, error.detail or "the protected credential operation failed"),
610
613
  )
611
614
 
612
615
 
@@ -622,6 +625,7 @@ __all__ = [
622
625
  "credentials_path",
623
626
  "default_credentials_dir",
624
627
  "install_credential_manifest",
628
+ "login_store_error",
625
629
  "read_credentials",
626
630
  "refuse_existing_credentials",
627
631
  "remove_credential_manifest_under_held_lock",
@@ -63,6 +63,7 @@ from mostlyright.data_harness.ux.credentials import (
63
63
  StoredCredentials,
64
64
  credential_transaction_store,
65
65
  install_credential_manifest,
66
+ login_store_error,
66
67
  refuse_existing_credentials,
67
68
  )
68
69
 
@@ -529,7 +530,7 @@ def run_login(
529
530
  if not manifest_path.exists():
530
531
  install_credential_manifest(manifest_path, store.backend.name)
531
532
  except CredentialStoreError as error:
532
- raise LoginError("LOGIN_CREDENTIAL_RECOVERY_REQUIRED", error.detail) from None
533
+ raise login_store_error(error) from None
533
534
  live_transport = transport or UrlLibDeviceFlowTransport()
534
535
 
535
536
  authorization = request_device_code(resolved_cloud_url, transport=live_transport)
@@ -11,7 +11,7 @@ neither affordable nor honest:
11
11
  * **Families.** Every code prefix is assigned to a family -- 224 of them, plus 18 compound
12
12
  prefixes, across 38 families. A family is one paragraph: what this class of failure is about,
13
13
  where to look, and what to do. Every prefix has one, so no code can fail to say something useful.
14
- * **Curated codes.** The 102 failures a person actually meets get their own exact sentences, and
14
+ * **Curated codes.** The 103 failures a person actually meets get their own exact sentences, and
15
15
  where one command is the whole answer, the command itself.
16
16
 
17
17
  Those four counts are not a note somebody remembered to update:
@@ -1770,6 +1770,19 @@ CODES: dict[str, Remediation] = {
1770
1770
  "mr-data login --force.",
1771
1771
  ),
1772
1772
  ),
1773
+ # `mr-data` narrows a credential directory it already owns, so what is left for this code to
1774
+ # report is something it may not touch on its own.
1775
+ "LOGIN_CREDENTIAL_STORE_PERMISSIONS": Remediation(
1776
+ lines=(
1777
+ "The place the device credential is kept is not a private folder this account owns, "
1778
+ "so nothing was read from it or written to it.",
1779
+ "That place is ~/.mostlyright. A folder there that you own is narrowed to your "
1780
+ "account automatically, so the usual causes are a link, a file, or a folder of "
1781
+ "another account at that name, or a volume that does not accept the change.",
1782
+ "Look at what sits at that path and who owns it. Put a folder you own there, then "
1783
+ "run mr-data login again.",
1784
+ ),
1785
+ ),
1773
1786
  "LOGIN_CREDENTIALS_UNREADABLE": Remediation(
1774
1787
  lines=(
1775
1788
  "The stored credential at the path named above exists but cannot be read as the file "