doover-cli 0.6.6__tar.gz → 0.6.7__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.6 → doover_cli-0.6.7}/PKG-INFO +2 -2
  2. {doover_cli-0.6.6 → doover_cli-0.6.7}/pyproject.toml +1 -1
  3. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/__init__.py +7 -1
  4. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/apps.py +32 -0
  5. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/migrate.py +1 -1
  6. doover_cli-0.6.7/src/doover_cli/notification_schema.py +153 -0
  7. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/apps.py +8 -0
  8. {doover_cli-0.6.6 → doover_cli-0.6.7}/uv.lock +4 -4
  9. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/CONTRIBUTING.md +0 -0
  10. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  11. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  12. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  13. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/workflows/lint-test.yml +0 -0
  15. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/workflows/release-brew.yml +0 -0
  16. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/workflows/release-pypi.yml +0 -0
  17. {doover_cli-0.6.6 → doover_cli-0.6.7}/.github/workflows/release.yml +0 -0
  18. {doover_cli-0.6.6 → doover_cli-0.6.7}/.gitignore +0 -0
  19. {doover_cli-0.6.6 → doover_cli-0.6.7}/.pre-commit-config.yaml +0 -0
  20. {doover_cli-0.6.6 → doover_cli-0.6.7}/AGENT.md +0 -0
  21. {doover_cli-0.6.6 → doover_cli-0.6.7}/LICENSE +0 -0
  22. {doover_cli-0.6.6 → doover_cli-0.6.7}/README.md +0 -0
  23. {doover_cli-0.6.6 → doover_cli-0.6.7}/debian/changelog +0 -0
  24. {doover_cli-0.6.6 → doover_cli-0.6.7}/debian/control +0 -0
  25. {doover_cli-0.6.6 → doover_cli-0.6.7}/debian/rules +0 -0
  26. {doover_cli-0.6.6 → doover_cli-0.6.7}/docs/user_stories.md +0 -0
  27. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/agent.py +0 -0
  28. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/api/__init__.py +0 -0
  29. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/api/auth.py +0 -0
  30. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/api/errors.py +0 -0
  31. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/api/session.py +0 -0
  32. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/app_install.py +0 -0
  33. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/device.py +0 -0
  34. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/device_type.py +0 -0
  35. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/tunnel.py +0 -0
  36. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/apps/widget_plugin.py +0 -0
  37. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/channel.py +0 -0
  38. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/colours.py +0 -0
  39. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/config_schema.py +0 -0
  40. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/dda_logs.py +0 -0
  41. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/doover_config.py +0 -0
  42. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/grpc.py +0 -0
  43. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/login.py +0 -0
  44. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/registry.py +0 -0
  45. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/renderer/__init__.py +0 -0
  46. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/renderer/_base.py +0 -0
  47. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/renderer/_basic.py +0 -0
  48. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/renderer/_default.py +0 -0
  49. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/renderer/_json.py +0 -0
  50. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/report.py +0 -0
  51. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/simulator.py +0 -0
  52. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/ui_schema.py +0 -0
  53. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/user.py +0 -0
  54. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/__init__.py +0 -0
  55. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/api.py +0 -0
  56. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/context.py +0 -0
  57. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/__init__.py +0 -0
  58. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/commands.py +0 -0
  59. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/lookup.py +0 -0
  60. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/prompting.py +0 -0
  61. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/schema.py +0 -0
  62. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/crud/values.py +0 -0
  63. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/errors.py +0 -0
  64. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/formatters.py +0 -0
  65. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/misc.py +0 -0
  66. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/parsers.py +0 -0
  67. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/prompt.py +0 -0
  68. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/sentry.py +0 -0
  69. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/shell_commands.py +0 -0
  70. {doover_cli-0.6.6 → doover_cli-0.6.7}/src/doover_cli/utils/state.py +0 -0
  71. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/__init__.py +0 -0
  72. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/conftest.py +0 -0
  73. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_agent.py +0 -0
  74. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_app_install.py +0 -0
  75. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_apps.py +0 -0
  76. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_auth_integration.py +0 -0
  77. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_auth_unit.py +0 -0
  78. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_basic.py +0 -0
  79. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_crud_commands.py +0 -0
  80. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_crud_lookup.py +0 -0
  81. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_crud_prompting.py +0 -0
  82. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_crud_schema.py +0 -0
  83. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_crud_values.py +0 -0
  84. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_default_renderer.py +0 -0
  85. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_device.py +0 -0
  86. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_device_cli_integration.py +0 -0
  87. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_device_type.py +0 -0
  88. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_device_type_cli_integration.py +0 -0
  89. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_discover.py +0 -0
  90. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_migrate.py +0 -0
  91. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_prompt.py +0 -0
  92. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_registry.py +0 -0
  93. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_schema_export.py +0 -0
  94. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_sentry.py +0 -0
  95. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_user.py +0 -0
  96. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_utils_apps.py +0 -0
  97. {doover_cli-0.6.6 → doover_cli-0.6.7}/tests/test_widget_plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: doover-cli
3
- Version: 0.6.6
3
+ Version: 0.6.7
4
4
  Summary: CLI for Doover
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -10,7 +10,7 @@ Requires-Dist: docker>=7.1.0
10
10
  Requires-Dist: jsf>=0.11.2
11
11
  Requires-Dist: paramiko>=3.5.1
12
12
  Requires-Dist: prompt-toolkit>=3.0.52
13
- Requires-Dist: pydoover>=1.12.0
13
+ Requires-Dist: pydoover>=1.17.1
14
14
  Requires-Dist: pytz>=2025.2
15
15
  Requires-Dist: questionary>=1.10.0
16
16
  Requires-Dist: requests>=2.32.3
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "docker>=7.1.0",
11
11
  "jsf>=0.11.2",
12
12
  "paramiko>=3.5.1",
13
- "pydoover>=1.12.0",
13
+ "pydoover>=1.17.1",
14
14
  "pytz>=2025.2",
15
15
  "questionary>=1.10.0",
16
16
  "requests>=2.32.3",
@@ -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.6"
4
+ __version__ = "0.6.7"
5
5
 
6
6
  import click
7
7
  import typer
@@ -11,6 +11,7 @@ from pydoover.api.auth import ConfigManager
11
11
  from .apps.apps import app as apps_app
12
12
  from .apps.app_install import app as app_install_app
13
13
  from .config_schema import app as config_schema_app
14
+ from .notification_schema import app as notification_schema_app
14
15
  from .ui_schema import app as ui_schema_app
15
16
  from .simulator import app as simulators_app
16
17
  from .agent import app as agents_app
@@ -40,6 +41,11 @@ app.add_typer(
40
41
  config_schema_app, name="config-schema", help="Manage application config schemas."
41
42
  )
42
43
  app.add_typer(ui_schema_app, name="ui-schema", help="Manage application UI schemas.")
44
+ app.add_typer(
45
+ notification_schema_app,
46
+ name="notification-schema",
47
+ help="Manage application notification schemas.",
48
+ )
43
49
  app.add_typer(
44
50
  simulators_app, name="simulator", help="Manage simulators and their configurations."
45
51
  )
@@ -26,6 +26,7 @@ import questionary
26
26
 
27
27
 
28
28
  from ..config_schema import export as export_config_command
29
+ from ..notification_schema import export as export_notification_command
29
30
  from ..ui_schema import export as export_ui_command
30
31
  from ..utils.api import ProfileAnnotation
31
32
  from ..registry import is_doover_registry, login_for_push, publish_github_output
@@ -397,6 +398,16 @@ def _build_application_payload(
397
398
  return payload
398
399
 
399
400
 
401
+ def _should_export_notifications(app_config) -> bool:
402
+ """Whether to try the notification exporter for this app.
403
+
404
+ Opt-out rather than opt-in so an app that adds notifications starts
405
+ publishing them without also having to remember a flag, but explicitly
406
+ skippable with ``"export_notification_command": "NO_EXPORT"``.
407
+ """
408
+ return getattr(app_config, "export_notification_command", None) != "NO_EXPORT"
409
+
410
+
400
411
  def _should_export_ui(app_config) -> bool:
401
412
  return app_config.type not in ("REP", "INT") and getattr(
402
413
  app_config, "generate_ui", True
@@ -1154,6 +1165,12 @@ def publish(
1154
1165
  help="Export the application UI schema before publishing.",
1155
1166
  ),
1156
1167
  ] = True,
1168
+ export_notifications: Annotated[
1169
+ bool,
1170
+ typer.Option(
1171
+ help="Export the application notification schema before publishing.",
1172
+ ),
1173
+ ] = True,
1157
1174
  build_widget: Annotated[
1158
1175
  bool,
1159
1176
  typer.Option(
@@ -1255,6 +1272,21 @@ def publish(
1255
1272
  else:
1256
1273
  rich.print("[green]Exported UI schema.[/green]")
1257
1274
 
1275
+ # Most apps declare no notifications, and the exporter is only present
1276
+ # on those that do, so a missing one is not worth a prompt -- unlike the
1277
+ # config and UI exports, which every app of their type has.
1278
+ if export_notifications and _should_export_notifications(app_config):
1279
+ try:
1280
+ ctx.invoke(
1281
+ export_notification_command, ctx, app_fp=root_fp, validate_=True
1282
+ )
1283
+ except Exception as exc:
1284
+ rich.print(
1285
+ f"[yellow]Skipping notification schema export: {exc}[/yellow]"
1286
+ )
1287
+ else:
1288
+ rich.print("[green]Exported notification schema.[/green]")
1289
+
1258
1290
  app_config = get_app_config(root_fp)
1259
1291
  resolved_staging = _resolve_staging(staging)
1260
1292
  payload = _build_application_payload(
@@ -58,7 +58,7 @@ REGISTRY_PROFILE_IDS = {
58
58
 
59
59
  # Generated from the app's Python/Rust at publish time. A committed copy is a
60
60
  # stale duplicate that nothing reads.
61
- GENERATED_FIELDS = ("config_schema", "ui_schema")
61
+ GENERATED_FIELDS = ("config_schema", "ui_schema", "notification_schema")
62
62
 
63
63
  # Container registry credentials are minted by the control plane against the
64
64
  # app's publish permission now, so a registry profile no longer selects
@@ -0,0 +1,153 @@
1
+ import json
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import rich
7
+ import typer
8
+
9
+ from .utils.apps import (
10
+ app_names_in_config,
11
+ get_app_directory,
12
+ get_app_config,
13
+ preserve_file,
14
+ run_schema_export,
15
+ )
16
+
17
+ app = typer.Typer(no_args_is_help=True)
18
+
19
+
20
+ @app.command()
21
+ def export(
22
+ ctx: typer.Context,
23
+ app_fp: Annotated[
24
+ Path, typer.Argument(help="Path to the application directory.")
25
+ ] = Path(),
26
+ validate_: Annotated[
27
+ bool,
28
+ typer.Option(
29
+ "--validate",
30
+ help="Validate the notification schema before exporting.",
31
+ ),
32
+ ] = True,
33
+ app_name: Annotated[
34
+ str | None,
35
+ typer.Option(
36
+ help="Application name in doover_config.json. Required to pick one "
37
+ "non-interactively when the repo defines multiple apps.",
38
+ ),
39
+ ] = None,
40
+ ):
41
+ """Export the application notification schema to the doover config json file."""
42
+ root_fp = get_app_directory(app_fp)
43
+ app_config = get_app_config(root_fp, app_name=app_name)
44
+ export_command = app_config.export_notification_command or "export-notifications"
45
+ if export_command == "NO_EXPORT":
46
+ print("App requested no notification export. Skipping...")
47
+ return
48
+
49
+ run_schema_export(
50
+ root_fp,
51
+ app_config.export_notification_command,
52
+ "export-notifications",
53
+ app_name=app_config.name,
54
+ cwd=app_fp,
55
+ )
56
+
57
+ print("Exporting notification schema...")
58
+
59
+ if validate_ is True:
60
+ print("Validating notification schema...")
61
+ ctx.invoke(validate, ctx, app_fp=app_fp, export_=False)
62
+
63
+
64
+ @app.command()
65
+ def validate(
66
+ ctx: typer.Context,
67
+ app_fp: Annotated[
68
+ Path, typer.Argument(help="Path to the application directory.")
69
+ ] = Path(),
70
+ export_: Annotated[
71
+ bool,
72
+ typer.Option(
73
+ "--export/--no-export",
74
+ help="Regenerate the schema from the source before validating. "
75
+ "The export is transient -- doover_config.json is restored "
76
+ "afterwards, so validation never writes to the working tree.",
77
+ ),
78
+ ] = True,
79
+ app_name: Annotated[
80
+ str | None,
81
+ typer.Option(
82
+ help="Application name in doover_config.json. Required to pick one "
83
+ "non-interactively when the repo defines multiple apps.",
84
+ ),
85
+ ] = None,
86
+ ):
87
+ """Validate the application notification schema."""
88
+ root_fp = get_app_directory(app_fp)
89
+ config_file = root_fp / "doover_config.json"
90
+
91
+ if export_ is True:
92
+ targets = [app_name] if app_name else app_names_in_config(root_fp) or [None]
93
+ with preserve_file(config_file):
94
+ for target in targets:
95
+ ctx.invoke(
96
+ export, ctx, app_fp=root_fp, validate_=False, app_name=target
97
+ )
98
+ _validate_notification_file(config_file)
99
+ else:
100
+ _validate_notification_file(config_file)
101
+
102
+
103
+ #: Every event a topic is built from has to survive being a topic segment, and
104
+ #: the policy decides which half of the hierarchy it lands in. Both are checked
105
+ #: here because a bad one is only otherwise discovered when a device in the
106
+ #: field tries to send the notification.
107
+ _VALID_POLICIES = ("default", "opt-in")
108
+
109
+
110
+ def _validate_notification_file(config_file: Path):
111
+ if not config_file.exists():
112
+ raise FileNotFoundError(
113
+ "doover_config.json not found. Please ensure there is a "
114
+ "doover_config.json file in the application directory."
115
+ )
116
+ data = json.loads(config_file.read_text())
117
+
118
+ for k, v in data.items():
119
+ if not isinstance(v, dict):
120
+ continue
121
+
122
+ schema = v.get("notification_schema")
123
+ # An explicit null says the same thing as no key: this app declares no
124
+ # notifications. Most apps do not, and an exporter that writes null is
125
+ # how they say so.
126
+ if schema is None:
127
+ continue
128
+
129
+ if not isinstance(schema, dict):
130
+ rich.print(
131
+ f"[red]Notification schema for {k} is not a JSON object "
132
+ f"(got {type(schema).__name__}).[/red]"
133
+ )
134
+ raise typer.Exit(1)
135
+
136
+ for event, entry in schema.items():
137
+ if not isinstance(entry, dict):
138
+ rich.print(
139
+ f"[red]Notification {event!r} for {k} is not a JSON object.[/red]"
140
+ )
141
+ raise typer.Exit(1)
142
+ policy = entry.get("policy")
143
+ if policy is not None and policy not in _VALID_POLICIES:
144
+ rich.print(
145
+ f"[red]Notification {event!r} for {k} has policy {policy!r}; "
146
+ f"expected one of {', '.join(_VALID_POLICIES)}.[/red]"
147
+ )
148
+ raise typer.Exit(1)
149
+
150
+ rich.print(
151
+ f"[green]Notification schema for {k} is valid "
152
+ f"({len(schema)} notification{'' if len(schema) == 1 else 's'}).[/green]"
153
+ )
@@ -111,6 +111,7 @@ class LocalApplication(ControlApplication):
111
111
  allow_many: bool | None = None,
112
112
  config_schema: Any | None = None,
113
113
  ui_schema: Any | None = None,
114
+ notification_schema: Any | None = None,
114
115
  depends_on: list[str] | None = None,
115
116
  organisation: dict[str, Any] | str | int | None = None,
116
117
  approx_installs: int | None = None,
@@ -127,6 +128,7 @@ class LocalApplication(ControlApplication):
127
128
  build_widget_command: str | None = None,
128
129
  export_config_command: str | None = None,
129
130
  export_ui_command: str | None = None,
131
+ export_notification_command: str | None = None,
130
132
  generate_ui: bool = True,
131
133
  run_command: str | None = None,
132
134
  staging_config: dict[str, Any] | None = None,
@@ -145,6 +147,7 @@ class LocalApplication(ControlApplication):
145
147
  allow_many=allow_many,
146
148
  config_schema=config_schema,
147
149
  ui_schema=ui_schema,
150
+ notification_schema=notification_schema,
148
151
  depends_on=depends_on,
149
152
  organisation=organisation,
150
153
  approx_installs=approx_installs,
@@ -181,6 +184,7 @@ class LocalApplication(ControlApplication):
181
184
  self.build_args = build_args
182
185
  self.export_config_command = export_config_command
183
186
  self.export_ui_command = export_ui_command
187
+ self.export_notification_command = export_notification_command
184
188
  self.generate_ui = generate_ui
185
189
  self.run_command = run_command
186
190
  self.staging_config = staging_config or {}
@@ -306,10 +310,12 @@ class LocalApplication(ControlApplication):
306
310
  build_widget_command=data.get("build_widget_command"),
307
311
  export_config_command=data.get("export_config_command"),
308
312
  export_ui_command=data.get("export_ui_command"),
313
+ export_notification_command=data.get("export_notification_command"),
309
314
  generate_ui=data.get("generate_ui", True),
310
315
  run_command=data.get("run_command"),
311
316
  config_schema=data.get("config_schema"),
312
317
  ui_schema=data.get("ui_schema"),
318
+ notification_schema=data.get("notification_schema"),
313
319
  staging_config=data.get("staging_config", {}),
314
320
  icon_url=data.get("icon_url"),
315
321
  banner_url=data.get("banner_url"),
@@ -401,6 +407,7 @@ class LocalApplication(ControlApplication):
401
407
  "lambda_config": self.lambda_config,
402
408
  "config_schema": self.config_schema,
403
409
  "ui_schema": self.ui_schema,
410
+ "notification_schema": self.notification_schema,
404
411
  "icon_url": self.icon_url,
405
412
  "banner_url": self.banner_url,
406
413
  }
@@ -417,6 +424,7 @@ class LocalApplication(ControlApplication):
417
424
  data["build_widget_command"] = self.build_widget_command
418
425
  data["export_config_command"] = self.export_config_command
419
426
  data["export_ui_command"] = self.export_ui_command
427
+ data["export_notification_command"] = self.export_notification_command
420
428
  data["generate_ui"] = self.generate_ui
421
429
  data["run_command"] = self.run_command
422
430
 
@@ -591,7 +591,7 @@ requires-dist = [
591
591
  { name = "jsf", specifier = ">=0.11.2" },
592
592
  { name = "paramiko", specifier = ">=3.5.1" },
593
593
  { name = "prompt-toolkit", specifier = ">=3.0.52" },
594
- { name = "pydoover", specifier = ">=1.12.0" },
594
+ { name = "pydoover", specifier = ">=1.17.1" },
595
595
  { name = "pytz", specifier = ">=2025.2" },
596
596
  { name = "questionary", specifier = ">=1.10.0" },
597
597
  { name = "requests", specifier = ">=2.32.3" },
@@ -1337,15 +1337,15 @@ wheels = [
1337
1337
 
1338
1338
  [[package]]
1339
1339
  name = "pydoover"
1340
- version = "1.12.0"
1340
+ version = "1.17.1"
1341
1341
  source = { registry = "https://pypi.org/simple" }
1342
1342
  dependencies = [
1343
1343
  { name = "aiohttp" },
1344
1344
  { name = "httpx" },
1345
1345
  ]
1346
- sdist = { url = "https://files.pythonhosted.org/packages/20/6a/6250ccb49522966c7024f7cfdda6268941ca8eeeaa32c7ecb3e983e5fd1a/pydoover-1.12.0.tar.gz", hash = "sha256:ef3c4f89ad81ed46f457a0fc72052e1da609246977745f7530bd9ae653dc1771", size = 563024, upload-time = "2026-07-31T02:23:39.204Z" }
1346
+ sdist = { url = "https://files.pythonhosted.org/packages/d9/99/56164d2368604d81dedde8f969f777f2cfa75b6325cd7235c4a64413714d/pydoover-1.17.1.tar.gz", hash = "sha256:11ae6055a5af53aeb137c5a14fe38942596f7549cd9bdf20db571433f135503d", size = 608525, upload-time = "2026-09-09T03:17:32.98Z" }
1347
1347
  wheels = [
1348
- { url = "https://files.pythonhosted.org/packages/44/c4/d70ae64e37d5ff8d68d9a833ec8771ffc30946174e453286b537ee418add/pydoover-1.12.0-py3-none-any.whl", hash = "sha256:d0b82371c7ad11b247aa278b3198ea7ea2bec4dc3b6886221ab33867acc6928f", size = 299364, upload-time = "2026-07-31T02:23:37.73Z" },
1348
+ { url = "https://files.pythonhosted.org/packages/2b/8f/f17d1242881565ed5d93604dab48977891421f3d1a1fe189f6150af31ae8/pydoover-1.17.1-py3-none-any.whl", hash = "sha256:dc9e6a67007d161193372d884b691809c7c835409f8c50d6dacbec5dee0ce27a", size = 329229, upload-time = "2026-09-09T03:17:31.696Z" },
1349
1349
  ]
1350
1350
 
1351
1351
  [[package]]
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