doover-cli 0.6.7__tar.gz → 0.6.8__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 (97) hide show
  1. {doover_cli-0.6.7 → doover_cli-0.6.8}/PKG-INFO +1 -1
  2. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/__init__.py +1 -1
  3. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/config_schema.py +5 -0
  4. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/notification_schema.py +2 -2
  5. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/apps.py +65 -10
  6. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_schema_export.py +117 -2
  7. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/CONTRIBUTING.md +0 -0
  8. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  9. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  10. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  11. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  12. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/workflows/lint-test.yml +0 -0
  13. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/workflows/release-brew.yml +0 -0
  14. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/workflows/release-pypi.yml +0 -0
  15. {doover_cli-0.6.7 → doover_cli-0.6.8}/.github/workflows/release.yml +0 -0
  16. {doover_cli-0.6.7 → doover_cli-0.6.8}/.gitignore +0 -0
  17. {doover_cli-0.6.7 → doover_cli-0.6.8}/.pre-commit-config.yaml +0 -0
  18. {doover_cli-0.6.7 → doover_cli-0.6.8}/AGENT.md +0 -0
  19. {doover_cli-0.6.7 → doover_cli-0.6.8}/LICENSE +0 -0
  20. {doover_cli-0.6.7 → doover_cli-0.6.8}/README.md +0 -0
  21. {doover_cli-0.6.7 → doover_cli-0.6.8}/debian/changelog +0 -0
  22. {doover_cli-0.6.7 → doover_cli-0.6.8}/debian/control +0 -0
  23. {doover_cli-0.6.7 → doover_cli-0.6.8}/debian/rules +0 -0
  24. {doover_cli-0.6.7 → doover_cli-0.6.8}/docs/user_stories.md +0 -0
  25. {doover_cli-0.6.7 → doover_cli-0.6.8}/pyproject.toml +0 -0
  26. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/agent.py +0 -0
  27. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/api/__init__.py +0 -0
  28. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/api/auth.py +0 -0
  29. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/api/errors.py +0 -0
  30. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/api/session.py +0 -0
  31. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/app_install.py +0 -0
  32. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/apps.py +0 -0
  33. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/device.py +0 -0
  34. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/device_type.py +0 -0
  35. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/migrate.py +0 -0
  36. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/tunnel.py +0 -0
  37. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/apps/widget_plugin.py +0 -0
  38. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/channel.py +0 -0
  39. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/colours.py +0 -0
  40. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/dda_logs.py +0 -0
  41. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/doover_config.py +0 -0
  42. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/grpc.py +0 -0
  43. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/login.py +0 -0
  44. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/registry.py +0 -0
  45. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/renderer/__init__.py +0 -0
  46. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/renderer/_base.py +0 -0
  47. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/renderer/_basic.py +0 -0
  48. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/renderer/_default.py +0 -0
  49. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/renderer/_json.py +0 -0
  50. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/report.py +0 -0
  51. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/simulator.py +0 -0
  52. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/ui_schema.py +0 -0
  53. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/user.py +0 -0
  54. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/__init__.py +0 -0
  55. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/api.py +0 -0
  56. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/context.py +0 -0
  57. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/__init__.py +0 -0
  58. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/commands.py +0 -0
  59. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/lookup.py +0 -0
  60. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/prompting.py +0 -0
  61. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/schema.py +0 -0
  62. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/crud/values.py +0 -0
  63. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/errors.py +0 -0
  64. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/formatters.py +0 -0
  65. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/misc.py +0 -0
  66. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/parsers.py +0 -0
  67. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/prompt.py +0 -0
  68. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/sentry.py +0 -0
  69. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/shell_commands.py +0 -0
  70. {doover_cli-0.6.7 → doover_cli-0.6.8}/src/doover_cli/utils/state.py +0 -0
  71. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/__init__.py +0 -0
  72. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/conftest.py +0 -0
  73. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_agent.py +0 -0
  74. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_app_install.py +0 -0
  75. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_apps.py +0 -0
  76. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_auth_integration.py +0 -0
  77. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_auth_unit.py +0 -0
  78. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_basic.py +0 -0
  79. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_crud_commands.py +0 -0
  80. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_crud_lookup.py +0 -0
  81. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_crud_prompting.py +0 -0
  82. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_crud_schema.py +0 -0
  83. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_crud_values.py +0 -0
  84. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_default_renderer.py +0 -0
  85. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_device.py +0 -0
  86. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_device_cli_integration.py +0 -0
  87. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_device_type.py +0 -0
  88. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_device_type_cli_integration.py +0 -0
  89. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_discover.py +0 -0
  90. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_migrate.py +0 -0
  91. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_prompt.py +0 -0
  92. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_registry.py +0 -0
  93. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_sentry.py +0 -0
  94. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_user.py +0 -0
  95. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_utils_apps.py +0 -0
  96. {doover_cli-0.6.7 → doover_cli-0.6.8}/tests/test_widget_plugin.py +0 -0
  97. {doover_cli-0.6.7 → doover_cli-0.6.8}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: doover-cli
3
- Version: 0.6.7
3
+ Version: 0.6.8
4
4
  Summary: CLI for Doover
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,7 +1,7 @@
1
1
  from doover_cli.renderer import Renderer
2
2
  from typing import Annotated, Optional
3
3
 
4
- __version__ = "0.6.7"
4
+ __version__ = "0.6.8"
5
5
 
6
6
  import click
7
7
  import typer
@@ -14,6 +14,7 @@ from .utils.apps import (
14
14
  get_app_config,
15
15
  preserve_file,
16
16
  run_schema_export,
17
+ RUST_EXPORT_COMMAND,
17
18
  )
18
19
 
19
20
  app = typer.Typer(no_args_is_help=True)
@@ -65,6 +66,10 @@ def export(
65
66
  "export-config",
66
67
  app_name=app_config.name,
67
68
  cwd=app_fp,
69
+ # The one exporter that drives a Rust binary: its `export` writes
70
+ # every schema the app has, so the UI and notification exporters
71
+ # have nothing left to run.
72
+ rust_default=RUST_EXPORT_COMMAND,
68
73
  )
69
74
  else:
70
75
  config = get_app_config(app_fp, app_name=app_name)
@@ -46,16 +46,16 @@ def export(
46
46
  print("App requested no notification export. Skipping...")
47
47
  return
48
48
 
49
+ print("Exporting notification schema...")
49
50
  run_schema_export(
50
51
  root_fp,
51
52
  app_config.export_notification_command,
52
53
  "export-notifications",
54
+ optional=True,
53
55
  app_name=app_config.name,
54
56
  cwd=app_fp,
55
57
  )
56
58
 
57
- print("Exporting notification schema...")
58
-
59
59
  if validate_ is True:
60
60
  print("Validating notification schema...")
61
61
  ctx.invoke(validate, ctx, app_fp=app_fp, export_=False)
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import json
3
3
  import sys
4
+ import tomllib
4
5
  import base64
5
6
  import shutil
6
7
  import contextlib
@@ -624,40 +625,94 @@ def detect_language(app_dir: Path) -> str | None:
624
625
 
625
626
 
626
627
  # What a Rust app runs to regenerate its schemas. The framework gives every
627
- # application binary an `export [path] [--app-name NAME]` subcommand that writes
628
- # the config schema and, when the app has one, the UI schema -- one command for
629
- # both, with the same read-merge-write over doover_config.json that pydoover's
630
- # exporters do. Driven through cargo so it works from a plain checkout with
631
- # nothing built yet, which is the state CI is always in.
628
+ # application binary a single `export [path] [--app-name NAME]` subcommand that
629
+ # writes the config schema and, when the app has one, the UI schema -- with the
630
+ # same read-merge-write over doover_config.json that pydoover's exporters do.
631
+ # Driven through cargo so it works from a plain checkout with nothing built yet,
632
+ # which is the state CI is always in.
633
+ #
634
+ # One command for every schema, which is why only the config exporter passes it
635
+ # as its `rust_default`: see run_schema_export.
632
636
  RUST_EXPORT_COMMAND = "cargo run --quiet -- export doover_config.json"
633
637
 
634
638
 
639
+ def python_declares_script(app_dir: Path, script: str) -> bool | None:
640
+ """Whether `app_dir`'s pyproject.toml declares the `script` console script.
641
+
642
+ None when there is nothing to go on -- no pyproject.toml, or one this can't
643
+ read -- so a caller can fall back to running the script and finding out.
644
+ """
645
+ pyproject = app_dir / "pyproject.toml"
646
+ if not pyproject.exists():
647
+ return None
648
+ try:
649
+ data = tomllib.loads(pyproject.read_text())
650
+ except (tomllib.TOMLDecodeError, OSError):
651
+ return None
652
+
653
+ scripts = data.get("project", {}).get("scripts")
654
+ if not isinstance(scripts, dict):
655
+ # A pyproject with no scripts table declares no exporters at all, which
656
+ # is an answer rather than a gap.
657
+ return False
658
+ return script in scripts
659
+
660
+
635
661
  def run_schema_export(
636
662
  app_dir: Path,
637
663
  command: str | None,
638
664
  python_default: str,
639
665
  app_name: str | None = None,
640
666
  cwd: Path | None = None,
667
+ rust_default: str | None = None,
668
+ optional: bool = False,
641
669
  ) -> None:
642
670
  """Regenerate an app's schemas by running its own exporter.
643
671
 
644
- Python apps run a console script under uv, as they always have. Rust apps
645
- run their binary's built-in `export` subcommand, so the schema in
646
- doover_config.json comes from the source in both languages rather than from
647
- whatever was committed.
672
+ Python apps run a console script under uv, as they always have: one script
673
+ per schema, each with its own default.
674
+
675
+ Rust apps have no such split. The framework gives a binary one `export`
676
+ subcommand that writes every schema it has, so exactly one exporter drives
677
+ it -- the config one, which passes `rust_default`. The others do nothing for
678
+ a Rust app unless it names a command explicitly, because there is nothing
679
+ left for them to write and no second subcommand to call.
680
+
681
+ That asymmetry is not cosmetic. Inventing `cargo run -- export` as the
682
+ default for *any* schema kind means a repo whose binary is not a doover-rs
683
+ application runs it anyway: doover-device-agent's binary rejects `export`
684
+ and fails the publish, and doover-device-runtime's supervisor accepts any
685
+ argv, so it started supervising and hung a CI job for 75 minutes. Both had
686
+ already opted out of every exporter that existed when they were written; the
687
+ next one to be added found them again.
648
688
 
649
689
  `command` is the app's own `export_*_command`, honoured verbatim when set --
650
690
  an app whose binary is not what `cargo run` picks by default (a workspace
651
691
  with several) says so there rather than being unbuildable by CI.
652
692
  """
653
693
  if detect_language(app_dir) == "rs":
654
- cmd = command or RUST_EXPORT_COMMAND
694
+ cmd = command or rust_default
695
+ if cmd is None:
696
+ print(
697
+ "Rust app: its `export` subcommand writes every schema it has, "
698
+ "and the config export already ran it. Skipping..."
699
+ )
700
+ return
655
701
  # The binary otherwise falls back to APP_KEY or its own file name, which
656
702
  # is not the application name in a repo whose binary is spelled with
657
703
  # hyphens and whose app is spelled with underscores.
658
704
  if app_name and "--app-name" not in cmd:
659
705
  cmd = f"{cmd} --app-name {app_name}"
660
706
  run(cmd, cwd=app_dir)
707
+ elif (
708
+ optional
709
+ and command is None
710
+ and (python_declares_script(app_dir, python_default) is False)
711
+ ):
712
+ print(
713
+ f"App declares no `{python_default}` script, so it has no schema of "
714
+ "this kind. Skipping..."
715
+ )
661
716
  else:
662
717
  call_with_uv(command or python_default, in_shell=True, cwd=cwd)
663
718
 
@@ -60,7 +60,13 @@ class TestRunSchemaExport:
60
60
  def test_rust_app_runs_its_export_subcommand(self, tmp_path, calls):
61
61
  (tmp_path / "Cargo.toml").touch()
62
62
 
63
- apps_utils.run_schema_export(tmp_path, None, "export-config", app_name="foo")
63
+ apps_utils.run_schema_export(
64
+ tmp_path,
65
+ None,
66
+ "export-config",
67
+ app_name="foo",
68
+ rust_default=apps_utils.RUST_EXPORT_COMMAND,
69
+ )
64
70
 
65
71
  assert calls["uv"] == []
66
72
  cmd, cwd = calls["run"][0]
@@ -71,10 +77,44 @@ class TestRunSchemaExport:
71
77
  def test_rust_app_without_an_app_name_leaves_the_flag_off(self, tmp_path, calls):
72
78
  (tmp_path / "Cargo.toml").touch()
73
79
 
74
- apps_utils.run_schema_export(tmp_path, None, "export-config")
80
+ apps_utils.run_schema_export(
81
+ tmp_path, None, "export-config", rust_default=apps_utils.RUST_EXPORT_COMMAND
82
+ )
75
83
 
76
84
  assert "--app-name" not in calls["run"][0][0]
77
85
 
86
+ def test_a_rust_app_runs_nothing_for_a_second_schema_kind(self, tmp_path, calls):
87
+ """The UI and notification exporters, which pass no rust_default.
88
+
89
+ A doover-rs binary has one `export` that writes every schema it has, so
90
+ the config exporter already ran it. Inventing the same command again is
91
+ what hung doover-device-runtime for 75 minutes: its binary is a
92
+ supervisor that accepts any argv, so `export` started it supervising.
93
+ """
94
+ (tmp_path / "Cargo.toml").touch()
95
+
96
+ apps_utils.run_schema_export(tmp_path, None, "export-ui", app_name="foo")
97
+
98
+ assert calls["run"] == []
99
+ assert calls["uv"] == []
100
+
101
+ def test_a_rust_app_still_honours_an_explicit_command(self, tmp_path, calls):
102
+ """An app that really does have a second exporter says so."""
103
+ (tmp_path / "Cargo.toml").touch()
104
+
105
+ apps_utils.run_schema_export(
106
+ tmp_path, "cargo run --bin exporter -- ui", "export-ui", app_name="foo"
107
+ )
108
+
109
+ assert calls["run"][0][0].startswith("cargo run --bin exporter -- ui")
110
+
111
+ def test_a_python_app_is_unaffected_by_the_rust_default(self, tmp_path, calls):
112
+ (tmp_path / "pyproject.toml").touch()
113
+
114
+ apps_utils.run_schema_export(tmp_path, None, "export-ui", app_name="foo")
115
+
116
+ assert calls["uv"][0][0] == ("export-ui",)
117
+
78
118
  def test_a_configured_rust_command_wins(self, tmp_path, calls):
79
119
  """A workspace with several binaries names the one to run itself."""
80
120
  (tmp_path / "Cargo.toml").touch()
@@ -104,6 +144,81 @@ class TestRunSchemaExport:
104
144
  assert calls["run"][0][0].endswith("explicit")
105
145
 
106
146
 
147
+ class TestOptionalSchema:
148
+ """`optional=True` -- the notification schema, which most apps don't have.
149
+
150
+ An app declares its exporters in its own `[project.scripts]`, so the absence
151
+ of one there is the app saying it has no notifications. Running it anyway
152
+ meant `uv run export-notifications` failed on nearly every Python app: a red
153
+ error and a reported handled exception for the ordinary case.
154
+ """
155
+
156
+ def _pyproject(self, tmp_path, body):
157
+ (tmp_path / "pyproject.toml").write_text(body)
158
+ return tmp_path
159
+
160
+ def test_skipped_when_the_script_is_not_declared(self, tmp_path, calls):
161
+ self._pyproject(
162
+ tmp_path,
163
+ '[project.scripts]\nexport-config = "app.app_config:export"\n',
164
+ )
165
+
166
+ apps_utils.run_schema_export(
167
+ tmp_path, None, "export-notifications", app_name="foo", optional=True
168
+ )
169
+
170
+ assert calls["uv"] == []
171
+ assert calls["run"] == []
172
+
173
+ def test_run_when_the_script_is_declared(self, tmp_path, calls):
174
+ self._pyproject(
175
+ tmp_path,
176
+ "[project.scripts]\n"
177
+ 'export-notifications = "app.app_notifications:export"\n',
178
+ )
179
+
180
+ apps_utils.run_schema_export(
181
+ tmp_path, None, "export-notifications", app_name="foo", optional=True
182
+ )
183
+
184
+ assert calls["uv"][0][0] == ("export-notifications",)
185
+
186
+ def test_an_explicit_command_is_never_probed(self, tmp_path, calls):
187
+ """An app naming its own exporter has said it has one, whatever the
188
+ scripts table looks like -- a second app in the repo exports through a
189
+ differently-named script."""
190
+ self._pyproject(tmp_path, '[project]\nname = "app"\n')
191
+
192
+ apps_utils.run_schema_export(
193
+ tmp_path,
194
+ "export-notifications-processor",
195
+ "export-notifications",
196
+ app_name="foo",
197
+ optional=True,
198
+ )
199
+
200
+ assert calls["uv"][0][0] == ("export-notifications-processor",)
201
+
202
+ def test_an_unreadable_pyproject_still_runs_the_export(self, tmp_path, calls):
203
+ """No answer is not the same as `no`: run it and let it fail loudly."""
204
+ self._pyproject(tmp_path, "this is not valid toml [[[")
205
+
206
+ apps_utils.run_schema_export(
207
+ tmp_path, None, "export-notifications", app_name="foo", optional=True
208
+ )
209
+
210
+ assert calls["uv"][0][0] == ("export-notifications",)
211
+
212
+ def test_a_required_schema_is_run_regardless(self, tmp_path, calls):
213
+ """Config and UI are not optional: every app of their type has one, so a
214
+ missing script there is a real failure worth surfacing."""
215
+ self._pyproject(tmp_path, '[project]\nname = "app"\n')
216
+
217
+ apps_utils.run_schema_export(tmp_path, None, "export-config", app_name="foo")
218
+
219
+ assert calls["uv"][0][0] == ("export-config",)
220
+
221
+
107
222
  class TestNoExport:
108
223
  """`export_config_command: NO_EXPORT` -- an app that holds no config of its
109
224
  own. Publishing one used to require giving it an exporter that wrote
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes