django-spec-cli 0.4.1__tar.gz → 0.5.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 (99) hide show
  1. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/workflows/release.yml +1 -1
  2. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/workflows/tests.yml +4 -4
  3. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/workflows/upstream-drift.yml +2 -2
  4. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/CHANGELOG.md +22 -1
  5. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/CLAUDE.md +1 -1
  6. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/PKG-INFO +3 -3
  7. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/README.md +1 -1
  8. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/__init__.py +2 -0
  9. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/spec_command.py +24 -4
  10. django_spec_cli-0.5.0/django_spec_cli/types/__init__.py +9 -0
  11. django_spec_cli-0.5.0/django_spec_cli/types/cli_transport.py +13 -0
  12. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/version.py +1 -1
  13. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/docs/examples/commands.py +22 -2
  14. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/docs/index.md +41 -1
  15. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/docs/reference.md +1 -0
  16. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/pyproject.toml +4 -3
  17. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/docs/test_commands_examples.py +7 -0
  18. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_spec_command.py +237 -0
  19. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/uv.lock +52 -52
  20. django_spec_cli-0.4.1/django_spec_cli/types/__init__.py +0 -7
  21. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/CODEOWNERS +0 -0
  22. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/SECURITY.md +0 -0
  23. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.github/dependabot.yml +0 -0
  24. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.gitignore +0 -0
  25. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/.pre-commit-config.yaml +0 -0
  26. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/LICENSE +0 -0
  27. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/Makefile +0 -0
  28. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/apps.py +0 -0
  29. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/__init__.py +0 -0
  30. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/add_spec_arguments.py +0 -0
  31. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/describe_parameters.py +0 -0
  32. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/parameter_descriptor.py +0 -0
  33. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/parsed_arguments.py +0 -0
  34. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/argv/utils.py +0 -0
  35. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/checks.py +0 -0
  36. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/exit_code_for.py +0 -0
  37. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/py.typed +0 -0
  38. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/__init__.py +0 -0
  39. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/render_json_lines.py +0 -0
  40. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/render_result.py +0 -0
  41. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/render_table.py +0 -0
  42. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/utils.py +0 -0
  43. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/render/wants_table.py +0 -0
  44. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/report/__init__.py +0 -0
  45. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/report/unguarded_message.py +0 -0
  46. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/report/utils.py +0 -0
  47. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/settings.py +0 -0
  48. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/django_spec_cli/types/exit_code.py +0 -0
  49. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/docs/__init__.py +0 -0
  50. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/docs/examples/__init__.py +0 -0
  51. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/mkdocs.yml +0 -0
  52. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/scripts/check-changelog +0 -0
  53. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/scripts/release-publish.sh +0 -0
  54. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/__init__.py +0 -0
  55. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/__init__.py +0 -0
  56. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/test_add_spec_arguments.py +0 -0
  57. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/test_describe_parameters.py +0 -0
  58. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/test_parameter_descriptor.py +0 -0
  59. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/test_parsed_arguments.py +0 -0
  60. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/test_utils.py +0 -0
  61. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/argv/utils.py +0 -0
  62. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/__init__.py +0 -0
  63. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/apps.py +0 -0
  64. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/__init__.py +0 -0
  65. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/__init__.py +0 -0
  66. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/notes.py +0 -0
  67. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_bad_parser.py +0 -0
  68. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_broken_import.py +0 -0
  69. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_broken_init.py +0 -0
  70. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_empty.py +0 -0
  71. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_no_registry.py +0 -0
  72. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_plain.py +0 -0
  73. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/management/commands/unguarded_report.py +0 -0
  74. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/commands_app/unguarded_registries.py +0 -0
  75. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/conftest.py +0 -0
  76. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/conftest_settings.py +0 -0
  77. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/dispatch_app/__init__.py +0 -0
  78. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/dispatch_app/apps.py +0 -0
  79. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/dispatch_app/models.py +0 -0
  80. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/docs/__init__.py +0 -0
  81. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/docs/test_reference.py +0 -0
  82. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/__init__.py +0 -0
  83. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/test_render_json_lines.py +0 -0
  84. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/test_render_result.py +0 -0
  85. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/test_render_table.py +0 -0
  86. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/test_utils.py +0 -0
  87. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/test_wants_table.py +0 -0
  88. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/render/utils.py +0 -0
  89. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/report/__init__.py +0 -0
  90. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/report/test_unguarded_message.py +0 -0
  91. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/report/test_utils.py +0 -0
  92. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_apps.py +0 -0
  93. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_checks.py +0 -0
  94. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_exit_code_for.py +0 -0
  95. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_init.py +0 -0
  96. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/test_settings.py +0 -0
  97. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/types/__init__.py +0 -0
  98. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/types/test_exit_code.py +0 -0
  99. {django_spec_cli-0.4.1 → django_spec_cli-0.5.0}/tests/utils.py +0 -0
@@ -31,7 +31,7 @@ jobs:
31
31
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
32
  with:
33
33
  fetch-depth: 0
34
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
34
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
35
35
  with:
36
36
  enable-cache: true
37
37
  - name: Install
@@ -20,7 +20,7 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
22
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
23
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
24
24
  with:
25
25
  enable-cache: true
26
26
  - name: Install
@@ -37,7 +37,7 @@ jobs:
37
37
  runs-on: ubuntu-latest
38
38
  steps:
39
39
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
40
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
41
41
  with:
42
42
  enable-cache: true
43
43
  - name: Install with docs group
@@ -54,7 +54,7 @@ jobs:
54
54
  runs-on: ubuntu-latest
55
55
  steps:
56
56
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
57
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
58
58
  with:
59
59
  enable-cache: true
60
60
  # A floor is a claim about the *oldest* version that works, and a lockfile
@@ -171,7 +171,7 @@ jobs:
171
171
 
172
172
  steps:
173
173
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
174
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
174
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
175
175
  with:
176
176
  enable-cache: true
177
177
  - name: Install Python ${{ matrix.python-version }}
@@ -43,14 +43,14 @@ jobs:
43
43
  runs-on: ubuntu-latest
44
44
  steps:
45
45
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
46
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
47
47
  with:
48
48
  enable-cache: false
49
49
 
50
50
  # One cell, not the matrix. The matrix answers "does this combination
51
51
  # work"; this answers "has an upstream moved under us", and the newest
52
52
  # supported Python is where a new release lands first.
53
- - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
53
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
54
54
  with:
55
55
  python-version: "3.14"
56
56
 
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] — 2026-11-08
11
+
12
+ ### Added
13
+ - **A run stamps its transport: `"cli"`.** Every callable a `SpecCommand` run
14
+ reaches is now handed a `call_context` built by the kernel's
15
+ `build_call_context` and stamped last with `"cli"`, exported as
16
+ `CLI_TRANSPORT`, so a permission check, the Validator, a provider, the
17
+ service and the presenter read `call_transport(call_context) == "cli"` from
18
+ one dict. Before, the command handed `dispatch` none, and a callable reading
19
+ the key saw a missing one, which the kernel reads as an in-process caller.
20
+ The Validator's declaration the command reads for the options it parses is
21
+ asked under the stamped dict as well. `request`, `view`, `format` and
22
+ `action` are in the dict as `None`, which binds nothing different from a
23
+ missing key. The command line page says what a callable reads here.
24
+
25
+ ### Changed
26
+ - **The floor on `django-service-specs` rises from 0.25.0 to 0.29.0**, the
27
+ release that adds `build_call_context` and removes the shared `Transport`
28
+ enum. A project on an older kernel stays on 0.4.x.
29
+
10
30
  ## [0.4.1] — 2026-11-04
11
31
 
12
32
  ### Added
@@ -135,7 +155,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
135
155
  `django-service-specs>=0.10.0`, the first kernel release with every name the
136
156
  command reads at its root.
137
157
 
138
- [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.4.1...HEAD
158
+ [Unreleased]: https://github.com/Artui/django-spec-cli/compare/v0.5.0...HEAD
159
+ [0.5.0]: https://github.com/Artui/django-spec-cli/compare/v0.4.1...v0.5.0
139
160
  [0.4.1]: https://github.com/Artui/django-spec-cli/compare/v0.4.0...v0.4.1
140
161
  [0.4.0]: https://github.com/Artui/django-spec-cli/compare/v0.3.1...v0.4.0
141
162
  [0.3.1]: https://github.com/Artui/django-spec-cli/compare/v0.3.0...v0.3.1
@@ -139,7 +139,7 @@ configured and enforced. isort order is stdlib, third party, first party.
139
139
  | --- | --- | --- |
140
140
  | Python | 3.10 | 3.10 through 3.14 |
141
141
  | Django | 4.2 | 4.2, 5.0, 5.1, 5.2, 6.0, 6.1 |
142
- | django-service-specs | 0.25.0 | the newest release, and the floor in the `floor` job |
142
+ | django-service-specs | 0.29.0 | the newest release, and the floor in the `floor` job |
143
143
 
144
144
  The Python and Django floors are the kernel's, and cannot sit below them.
145
145
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-spec-cli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: Run a django-service-specs registry from manage.py: one subcommand per spec, options generated from its parameters, exit codes for every refusal.
5
5
  Project-URL: Homepage, https://github.com/Artui/django-spec-cli
6
6
  Project-URL: Repository, https://github.com/Artui/django-spec-cli
@@ -30,7 +30,7 @@ Classifier: Programming Language :: Python :: 3.13
30
30
  Classifier: Programming Language :: Python :: 3.14
31
31
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
32
32
  Requires-Python: >=3.10
33
- Requires-Dist: django-service-specs>=0.25.0
33
+ Requires-Dist: django-service-specs>=0.29.0
34
34
  Requires-Dist: django>=4.2
35
35
  Description-Content-Type: text/markdown
36
36
 
@@ -61,7 +61,7 @@ the import path, the system check ids and the setting key are this package's.
61
61
  pip install django-spec-cli
62
62
  ```
63
63
 
64
- Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.25.0 or
64
+ Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.29.0 or
65
65
  later, which it installs.
66
66
 
67
67
  ## Quickstart
@@ -25,7 +25,7 @@ the import path, the system check ids and the setting key are this package's.
25
25
  pip install django-spec-cli
26
26
  ```
27
27
 
28
- Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.25.0 or
28
+ Python 3.10 or later, Django 4.2 or later, and `django-service-specs` 0.29.0 or
29
29
  later, which it installs.
30
30
 
31
31
  ## Quickstart
@@ -11,11 +11,13 @@ from django_spec_cli.render.render_table import render_table
11
11
  from django_spec_cli.render.wants_table import wants_table
12
12
  from django_spec_cli.report.unguarded_message import unguarded_message
13
13
  from django_spec_cli.spec_command import SpecCommand
14
+ from django_spec_cli.types.cli_transport import CLI_TRANSPORT
14
15
  from django_spec_cli.types.exit_code import ExitCode
15
16
  from django_spec_cli.version import __version__
16
17
 
17
18
  __all__ = [
18
19
  "__version__",
20
+ "CLI_TRANSPORT",
19
21
  "ExitCode",
20
22
  "ParameterDescriptor",
21
23
  "SpecCommand",
@@ -26,6 +26,7 @@ from django_service_specs import (
26
26
  ServiceSpec,
27
27
  ServiceValidationError,
28
28
  SpecRegistry,
29
+ build_call_context,
29
30
  check_seeds,
30
31
  coerce_flat,
31
32
  dispatch,
@@ -46,6 +47,7 @@ from django_spec_cli.render.render_result import render_result
46
47
  from django_spec_cli.render.wants_table import wants_table
47
48
  from django_spec_cli.report.unguarded_message import unguarded_message
48
49
  from django_spec_cli.report.utils import refused_message
50
+ from django_spec_cli.types.cli_transport import CLI_TRANSPORT
49
51
  from django_spec_cli.types.exit_code import ExitCode
50
52
 
51
53
  _OWN_OPTIONS = frozenset({"--as-user", "--json", "--table"})
@@ -173,7 +175,7 @@ class SpecCommand(BaseCommand):
173
175
  )
174
176
  self._entries[name] = entry
175
177
  spec = kernel_spec(entry.spec)
176
- parameters = _sendable(spec, self.pool_seeds)
178
+ parameters = _sendable(spec, self.pool_seeds, _call_context())
177
179
  summary = _summary(spec, entry.name)
178
180
  subparser = subparsers.add_parser(
179
181
  name,
@@ -256,7 +258,11 @@ class SpecCommand(BaseCommand):
256
258
  refused_message(entry.name, self._command_label, error)
257
259
  ) from error
258
260
  spec = kernel_spec(entry.spec)
259
- parameters = spec.parameters()
261
+ # One dict for the whole run, built ahead of the first callable it
262
+ # asks: the declaration read here, then every stage ``dispatch`` runs,
263
+ # so a callable reading ``transport`` sees ``cli`` from either.
264
+ call_context = _call_context()
265
+ parameters = spec.parameters(call_context=call_context)
260
266
  raw = parsed_arguments(argparse.Namespace(**options), parameters)
261
267
  # Read before the run, not after it: a setting that names no format
262
268
  # raises, and raising once a write has committed reports a failure for
@@ -282,6 +288,7 @@ class SpecCommand(BaseCommand):
282
288
  principal=principal,
283
289
  arguments=coerce_flat(parameters, raw),
284
290
  pool_seeds=self.pool_seeds,
291
+ call_context=call_context,
285
292
  )
286
293
  except (DispatchError, ServiceError) as error:
287
294
  raise CommandError(_message(error), returncode=int(exit_code_for(error))) from error
@@ -304,7 +311,20 @@ class SpecCommand(BaseCommand):
304
311
  self.stdout.write(unguarded_message(unguarded, label))
305
312
 
306
313
 
307
- def _sendable(spec: ServiceSpec | SelectorSpec, pool_seeds: PoolSeeds) -> Parameters:
314
+ def _call_context() -> dict[str, Any]:
315
+ """A new context for the command's own reads and one run, stamped ``cli``.
316
+
317
+ ``request`` and ``view`` are there as ``None``, since a command line has
318
+ neither: the kernel hands a callable declaring either ``None`` for a
319
+ ``None`` entry exactly as for a missing one, so no binding changes.
320
+ ``format`` and ``action`` are no pool names and are never bound.
321
+ """
322
+ return build_call_context(CLI_TRANSPORT)
323
+
324
+
325
+ def _sendable(
326
+ spec: ServiceSpec | SelectorSpec, pool_seeds: PoolSeeds, call_context: dict[str, Any]
327
+ ) -> Parameters:
308
328
  """What a caller may send, as the kernel's caller schema reads it: required only where dispatch demands it.
309
329
 
310
330
  The parameters ``spec_caller_input_schema`` offers under the command's
@@ -327,7 +347,7 @@ def _sendable(spec: ServiceSpec | SelectorSpec, pool_seeds: PoolSeeds) -> Parame
327
347
  return Parameters.of(
328
348
  *(
329
349
  param if param.name in required else replace(param, required=False)
330
- for param in spec.parameters()
350
+ for param in spec.parameters(call_context=call_context)
331
351
  if param.name in schema["properties"]
332
352
  )
333
353
  )
@@ -0,0 +1,9 @@
1
+ """Value types: the exit status a command's run ends with, and the transport name it stamps."""
2
+
3
+ from django_spec_cli.types.cli_transport import CLI_TRANSPORT
4
+ from django_spec_cli.types.exit_code import ExitCode
5
+
6
+ __all__ = [
7
+ "CLI_TRANSPORT",
8
+ "ExitCode",
9
+ ]
@@ -0,0 +1,13 @@
1
+ """``CLI_TRANSPORT`` - the transport name a ``SpecCommand`` stamps on every call's context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ CLI_TRANSPORT: Final = "cli"
8
+ """What a ``SpecCommand`` stamps under the kernel's ``TRANSPORT_KEY``: the plain string ``"cli"``.
9
+
10
+ A callable reads it with the kernel's ``call_transport``, or compares
11
+ ``call_context["transport"] == "cli"`` with nothing imported. This package owns
12
+ the string and the kernel lists no transports; it is a public contract, so it
13
+ never changes."""
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__: str = "0.4.1"
5
+ __version__: str = "0.5.0"
6
6
 
7
7
  __all__ = ["__version__"]
@@ -1,5 +1,6 @@
1
- """A registry served as a management command, a command that looks its user up by username, and
2
- a command serving a spec the kernel refuses as declared.
1
+ """A registry served as a management command, a command that looks its user up by username, a
2
+ command whose service reads the transport it is called over, and a command serving a spec the
3
+ kernel refuses as declared.
3
4
 
4
5
  ``tests/docs/test_commands_examples.py`` runs every command line
5
6
  ``docs/index.md`` shows through this module's ``Command``, from argv as
@@ -31,6 +32,7 @@ from django_service_specs import (
31
32
  ServiceSpec,
32
33
  SpecRegistry,
33
34
  Unrestricted,
35
+ call_transport,
34
36
  )
35
37
 
36
38
  from django_spec_cli import SpecCommand
@@ -166,3 +168,21 @@ class Drafts(SpecCommand):
166
168
 
167
169
 
168
170
  # --8<-- [end:refused]
171
+
172
+
173
+ # --8<-- [start:transport]
174
+ def where(*, call_context: Any) -> str:
175
+ """The transport this call came over, as its context says."""
176
+ return call_transport(call_context) or "in-process"
177
+
178
+
179
+ where_registry = SpecRegistry()
180
+ where_registry.register("where", ServiceSpec(service=where, permissions=[IsSignedIn()]))
181
+
182
+
183
+ # yourapp/management/commands/where.py
184
+ class Where(SpecCommand):
185
+ registry = where_registry
186
+
187
+
188
+ # --8<-- [end:transport]
@@ -27,7 +27,7 @@ pip install django-spec-cli
27
27
  ```
28
28
 
29
29
  It installs [`django-service-specs`](https://artui.github.io/django-service-specs/), the kernel whose specs it serves,
30
- from 0.25.0, on Python 3.10 and Django 4.2 or later. The command line was the
30
+ from 0.29.0, on Python 3.10 and Django 4.2 or later. The command line was the
31
31
  kernel's `commands/` subpackage, and moved here so the kernel carries no
32
32
  transport.
33
33
 
@@ -296,6 +296,46 @@ preceded by the warnings for `whoami` and `retitle`. `call_command` skips
296
296
  system checks for every command but `check`, so a command run through it
297
297
  does not print them.
298
298
 
299
+ ## What a callable reads here
300
+
301
+ Every callable a run reaches is handed one `call_context`, built by the
302
+ kernel's
303
+ [`build_call_context`][django_service_specs.types.build_call_context.build_call_context]
304
+ and stamped last with this package's transport name, the plain string `"cli"`,
305
+ exported as `django_spec_cli.CLI_TRANSPORT`. Read it with the kernel's
306
+ [`call_transport`][django_service_specs.types.call_transport.call_transport],
307
+ or compare `call_context["transport"] == "cli"` with nothing imported:
308
+
309
+ ```python
310
+ --8<--
311
+ docs/examples/commands.py:transport
312
+ --8<--
313
+ ```
314
+
315
+ - **One dict per run.** The command builds it ahead of everything it asks of
316
+ a spec and hands the same object to the declaration it reads for the
317
+ options it parses, then to `dispatch`, so a permission check, the
318
+ Validator's `parameters()` and `validate()`, a provider, the service and the
319
+ presenter all read `"cli"` from one dict.
320
+ - **The standard entries are there as `None`.** `request`, `view`, `format`
321
+ and `action` are keys of the dict with no value, since a command line has
322
+ none. The kernel hands a callable declaring `request` or `view` `None` for
323
+ such an entry, as it does for a missing one, and `format` and `action` are
324
+ never bound by name, so a spec written for HTTP runs here as it did.
325
+ - **`--help` is read under a dict too.** The command builds a stamped dict of
326
+ its own for each spec to describe a Validator's fields for the parser. The
327
+ kernel's schema and seed checks that decide which options are offered are
328
+ asked outside a call, as they are by every transport, and read no stamp. So
329
+ the option names come from a transport-blind declaration: a field a
330
+ Validator declares only when `call_transport(call_context) == "cli"` is
331
+ never offered, and if it is required, every run is refused for it.
332
+ Declare such a field for every transport, or not at all.
333
+ - **Seams.** The value is not configurable: a subclass cannot change what a
334
+ run stamps. What a subclass can supply is `resolve_as_user`, who the run is
335
+ made as, and `pool_seeds`, which adds a keyword to the pool every callable
336
+ may declare, so an entry a project needs in every call is a seed rather
337
+ than a key of the dict.
338
+
299
339
  ## What it does not do
300
340
 
301
341
  - **No payload file.** Arguments are options; an object or a list of rows has
@@ -60,3 +60,4 @@ Registered by `DjangoSpecCliConfig`, so a project gets them by listing
60
60
  ## Types
61
61
 
62
62
  ::: django_spec_cli.types.exit_code.ExitCode
63
+ ::: django_spec_cli.types.cli_transport.CLI_TRANSPORT
@@ -48,8 +48,9 @@ dependencies = [
48
48
  # ``PermissionCheckDenied`` in ``REFUSALS``, which
49
49
  # ``tests/test_exit_code_for.py`` decides by name. Below 0.11.0 the kernel
50
50
  # also still shipped its own copy of the command and registered its own
51
- # ``W001`` and ``W003``.
52
- "django-service-specs>=0.25.0",
51
+ # ``W001`` and ``W003``. 0.29.0 adds ``build_call_context``, which the
52
+ # command builds the context it stamps ``cli`` on through.
53
+ "django-service-specs>=0.29.0",
53
54
  ]
54
55
 
55
56
  [project.urls]
@@ -149,7 +150,7 @@ skip-magic-trailing-comma = false
149
150
  root = ["./django_spec_cli"]
150
151
 
151
152
  [tool.bumpversion]
152
- current_version = "0.4.1"
153
+ current_version = "0.5.0"
153
154
  allow_dirty = false
154
155
  commit = false
155
156
  tag = false
@@ -172,3 +172,10 @@ def test_a_run_of_the_refused_spec_raises_the_page_s_sentence() -> None:
172
172
  def test_whoami_is_the_user_named(capsys: pytest.CaptureFixture[str]) -> None:
173
173
  assert _run_from_argv("whoami --as-user 1") == 0
174
174
  assert capsys.readouterr().out == '"ada"\n'
175
+
176
+
177
+ @pytest.mark.usefixtures("world")
178
+ def test_the_transport_example_reads_cli() -> None:
179
+ out = io.StringIO()
180
+ call_command(commands.Where(), "where", "--as-user", "1", stdout=out)
181
+ assert out.getvalue() == '"cli"\n'