arkitekt-next 0.7.8__py3-none-any.whl

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.

Potentially problematic release.


This version of arkitekt-next might be problematic. Click here for more details.

Files changed (119) hide show
  1. arkitekt_next/__init__.py +43 -0
  2. arkitekt_next/apps/__init__.py +3 -0
  3. arkitekt_next/apps/easy.py +99 -0
  4. arkitekt_next/apps/next.py +40 -0
  5. arkitekt_next/apps/qt.py +97 -0
  6. arkitekt_next/apps/service/__init__.py +3 -0
  7. arkitekt_next/apps/service/fakts.py +88 -0
  8. arkitekt_next/apps/service/fakts_next.py +79 -0
  9. arkitekt_next/apps/service/fakts_qt.py +82 -0
  10. arkitekt_next/apps/service/fluss_next.py +31 -0
  11. arkitekt_next/apps/service/grant_registry.py +27 -0
  12. arkitekt_next/apps/service/herre.py +24 -0
  13. arkitekt_next/apps/service/herre_qt.py +57 -0
  14. arkitekt_next/apps/service/kabinet.py +31 -0
  15. arkitekt_next/apps/service/mikro_next.py +81 -0
  16. arkitekt_next/apps/service/rekuest_next.py +53 -0
  17. arkitekt_next/apps/service/unlok_next.py +32 -0
  18. arkitekt_next/apps/types.py +53 -0
  19. arkitekt_next/builders.py +264 -0
  20. arkitekt_next/cli/__init__.py +0 -0
  21. arkitekt_next/cli/commands/call/__init__.py +0 -0
  22. arkitekt_next/cli/commands/call/local.py +132 -0
  23. arkitekt_next/cli/commands/call/main.py +22 -0
  24. arkitekt_next/cli/commands/call/remote.py +90 -0
  25. arkitekt_next/cli/commands/gen/__init__.py +0 -0
  26. arkitekt_next/cli/commands/gen/compile.py +45 -0
  27. arkitekt_next/cli/commands/gen/init.py +122 -0
  28. arkitekt_next/cli/commands/gen/main.py +29 -0
  29. arkitekt_next/cli/commands/gen/watch.py +32 -0
  30. arkitekt_next/cli/commands/init/__init__.py +0 -0
  31. arkitekt_next/cli/commands/init/main.py +194 -0
  32. arkitekt_next/cli/commands/inspect/__init__.py +0 -0
  33. arkitekt_next/cli/commands/inspect/definitions.py +53 -0
  34. arkitekt_next/cli/commands/inspect/main.py +22 -0
  35. arkitekt_next/cli/commands/inspect/variables.py +92 -0
  36. arkitekt_next/cli/commands/manifest/__init__.py +0 -0
  37. arkitekt_next/cli/commands/manifest/inspect.py +42 -0
  38. arkitekt_next/cli/commands/manifest/main.py +25 -0
  39. arkitekt_next/cli/commands/manifest/scopes.py +155 -0
  40. arkitekt_next/cli/commands/manifest/version.py +147 -0
  41. arkitekt_next/cli/commands/manifest/wizard.py +94 -0
  42. arkitekt_next/cli/commands/port/__init__.py +0 -0
  43. arkitekt_next/cli/commands/port/build.py +231 -0
  44. arkitekt_next/cli/commands/port/init.py +82 -0
  45. arkitekt_next/cli/commands/port/main.py +31 -0
  46. arkitekt_next/cli/commands/port/publish.py +102 -0
  47. arkitekt_next/cli/commands/port/stage.py +59 -0
  48. arkitekt_next/cli/commands/port/utils.py +47 -0
  49. arkitekt_next/cli/commands/port/validate.py +78 -0
  50. arkitekt_next/cli/commands/port/wizard.py +329 -0
  51. arkitekt_next/cli/commands/run/__init__.py +0 -0
  52. arkitekt_next/cli/commands/run/dev.py +349 -0
  53. arkitekt_next/cli/commands/run/main.py +22 -0
  54. arkitekt_next/cli/commands/run/prod.py +57 -0
  55. arkitekt_next/cli/commands/run/utils.py +10 -0
  56. arkitekt_next/cli/commands/server/__init__.py +0 -0
  57. arkitekt_next/cli/commands/server/down.py +56 -0
  58. arkitekt_next/cli/commands/server/init.py +74 -0
  59. arkitekt_next/cli/commands/server/inspect.py +59 -0
  60. arkitekt_next/cli/commands/server/main.py +33 -0
  61. arkitekt_next/cli/commands/server/open.py +66 -0
  62. arkitekt_next/cli/commands/server/remove.py +60 -0
  63. arkitekt_next/cli/commands/server/stop.py +56 -0
  64. arkitekt_next/cli/commands/server/up.py +70 -0
  65. arkitekt_next/cli/commands/server/utils.py +33 -0
  66. arkitekt_next/cli/configs/base.yaml +867 -0
  67. arkitekt_next/cli/constants.py +63 -0
  68. arkitekt_next/cli/dockerfiles/vanilla.dockerfile +8 -0
  69. arkitekt_next/cli/errors.py +4 -0
  70. arkitekt_next/cli/inspect.py +1 -0
  71. arkitekt_next/cli/io.py +255 -0
  72. arkitekt_next/cli/main.py +83 -0
  73. arkitekt_next/cli/options.py +166 -0
  74. arkitekt_next/cli/schemas/fluss.schema.graphql +2446 -0
  75. arkitekt_next/cli/schemas/gucker.schema.graphql +8908 -0
  76. arkitekt_next/cli/schemas/kabinet.schema.graphql +515 -0
  77. arkitekt_next/cli/schemas/kluster.schema.graphql +109 -0
  78. arkitekt_next/cli/schemas/konviktion.schema.graphql +70 -0
  79. arkitekt_next/cli/schemas/kuay.schema.graphql +356 -0
  80. arkitekt_next/cli/schemas/mikro.schema.graphql +8908 -0
  81. arkitekt_next/cli/schemas/mikro_next.schema.graphql +1639 -0
  82. arkitekt_next/cli/schemas/napari.schema.graphql +8908 -0
  83. arkitekt_next/cli/schemas/omero_ark.schema.graphql +100 -0
  84. arkitekt_next/cli/schemas/port.schema.graphql +356 -0
  85. arkitekt_next/cli/schemas/rekuest.schema.graphql +4630 -0
  86. arkitekt_next/cli/schemas/rekuest_next.schema.graphql +1159 -0
  87. arkitekt_next/cli/schemas/unlok.schema.graphql +1013 -0
  88. arkitekt_next/cli/templates/filter.py +26 -0
  89. arkitekt_next/cli/templates/simple.py +67 -0
  90. arkitekt_next/cli/texts.py +20 -0
  91. arkitekt_next/cli/types.py +365 -0
  92. arkitekt_next/cli/ui.py +111 -0
  93. arkitekt_next/cli/utils.py +15 -0
  94. arkitekt_next/cli/validators.py +17 -0
  95. arkitekt_next/cli/vars.py +39 -0
  96. arkitekt_next/cli/versions/v1.yaml +1 -0
  97. arkitekt_next/constants.py +6 -0
  98. arkitekt_next/model.py +110 -0
  99. arkitekt_next/qt/__init__.py +9 -0
  100. arkitekt_next/qt/assets/dark/gear.png +0 -0
  101. arkitekt_next/qt/assets/dark/green pulse.gif +0 -0
  102. arkitekt_next/qt/assets/dark/orange pulse.gif +0 -0
  103. arkitekt_next/qt/assets/dark/pink pulse.gif +0 -0
  104. arkitekt_next/qt/assets/dark/red pulse.gif +0 -0
  105. arkitekt_next/qt/assets/light/gear.png +0 -0
  106. arkitekt_next/qt/assets/light/green pulse.gif +0 -0
  107. arkitekt_next/qt/assets/light/orange pulse.gif +0 -0
  108. arkitekt_next/qt/assets/light/pink pulse.gif +0 -0
  109. arkitekt_next/qt/assets/light/red pulse.gif +0 -0
  110. arkitekt_next/qt/magic_bar.py +545 -0
  111. arkitekt_next/qt/utils.py +30 -0
  112. arkitekt_next/service_registry.py +51 -0
  113. arkitekt_next/tqdm.py +43 -0
  114. arkitekt_next/utils.py +38 -0
  115. arkitekt_next-0.7.8.dist-info/LICENSE +21 -0
  116. arkitekt_next-0.7.8.dist-info/METADATA +155 -0
  117. arkitekt_next-0.7.8.dist-info/RECORD +119 -0
  118. arkitekt_next-0.7.8.dist-info/WHEEL +4 -0
  119. arkitekt_next-0.7.8.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,329 @@
1
+ from arkitekt_next.cli.types import Manifest, Packager
2
+ from arkitekt_next.cli.vars import get_manifest
3
+ import sys
4
+ import rich_click as click
5
+ from click import Context
6
+ import subprocess
7
+ from rich import get_console
8
+
9
+ try:
10
+ pass
11
+ except ImportError as e:
12
+ raise ImportError("Please install rekuest to use this feature") from e
13
+
14
+
15
+ import os
16
+
17
+
18
+ def get_base_prefix_compat() -> str:
19
+ """Get base/real prefix, or sys.prefix if there is none."""
20
+ return (
21
+ getattr(sys, "base_prefix", None)
22
+ or getattr(sys, "real_prefix", None)
23
+ or sys.prefix
24
+ )
25
+
26
+
27
+ def in_virtualenv() -> bool:
28
+ """Returns True if in a virtualenv, otherwise False."""
29
+ return get_base_prefix_compat() != sys.prefix
30
+
31
+
32
+ def pip_no_gpu(manifest: Manifest, python_version: str) -> str:
33
+ """A function to generate a dockerfile for pip without gpu support
34
+
35
+ Parameters
36
+ ----------
37
+ manifest : Manifest
38
+ The manifest of the project
39
+ python_version : str
40
+ The python version to use
41
+
42
+ Returns
43
+ -------
44
+ str
45
+ The dockerfile
46
+ """
47
+ get_console().print("[blue]Setting up environment...[/blue]")
48
+ if not in_virtualenv():
49
+ click.confirm(
50
+ "You are not in a virtual environment. Consider using a virtual environment. Do you want to continue?",
51
+ abort=True,
52
+ )
53
+
54
+ if not os.path.exists(".requirements.txt") and click.confirm(
55
+ "Do you want to freeze the dependencies the current interpreter?"
56
+ ):
57
+ subprocess.check_call("pip freeze > requirements.txt", shell=True)
58
+
59
+ get_console().print("[blue]Generating Dockerfile...[/blue]")
60
+
61
+ BASE_IMAGE = f"FROM python:{python_version}\n"
62
+ PIP_APPENDIX = "# setup pip environment\nCOPY ./requirements.txt /tmp/requirements.txt\nRUN pip install /tmp/requirements.yml\n"
63
+ WORKDIR_APPENDIX = "RUN mkdir /app \nWORKDIR /app"
64
+
65
+ dockerfile = BASE_IMAGE
66
+ if click.confirm(
67
+ "Do you want to install a requirements.txt with the dependencies with pip?"
68
+ ):
69
+ dockerfile += PIP_APPENDIX
70
+
71
+ if click.confirm("Do you want to use the current working directory as the app?"):
72
+ dockerfile += WORKDIR_APPENDIX
73
+
74
+ return dockerfile
75
+
76
+
77
+ def poetry_no_gpu(manifest: Manifest, python_version: str) -> str:
78
+ """A function to generate a dockerfile for pip without gpu support
79
+
80
+ Parameters
81
+ ----------
82
+ manifest : Manifest
83
+ The manifest of the project
84
+ python_version : str
85
+ The python version to use
86
+
87
+ Returns
88
+ -------
89
+ str
90
+ The dockerfile
91
+ """
92
+ get_console().print("[blue]Setting up environment...[/blue]")
93
+ if not in_virtualenv():
94
+ click.confirm(
95
+ "You are not in a virtual environment. Consider using a virtual environment. Do you want to continue?",
96
+ abort=True,
97
+ )
98
+
99
+ if click.confirm("Do you want to lock the current dependencies?"):
100
+ subprocess.check_call("poetry lock", shell=True)
101
+
102
+ get_console().print("[blue]Generating Dockerfile...[/blue]")
103
+
104
+ BASE_IMAGE = f"FROM python:{python_version}\n"
105
+ SETUP_POETRY = "# Install dependencies\nRUN pip install poetry rich\nENV PYTHONUNBUFFERED=1\n# Install Project files\nCOPY pyproject.toml /tmp\nCOPY poetry.lock /tmp\nRUN poetry config virtualenvs.create false\nWORKDIR /tmp\nRUN poetry install\n"
106
+ WORKDIR_APPENDIX = "RUN mkdir /app\nCOPY . /app\nWORKDIR /app\n"
107
+
108
+ dockerfile = BASE_IMAGE
109
+ dockerfile += SETUP_POETRY
110
+
111
+ if click.confirm("Do you want to use the current working directory as the app?"):
112
+ dockerfile += WORKDIR_APPENDIX
113
+
114
+ return dockerfile
115
+
116
+
117
+ CONDA_GPU = """
118
+ FROM jhnnsrs/tensorflow_conda_gpu:latest
119
+
120
+ # setup conda virtual environment
121
+ COPY ./requirements.yml /tmp/requirements.yml
122
+ RUN conda env create --name camera-seg -f /tmp/requirements.yml
123
+
124
+ RUN conda activate camera-seg
125
+ """
126
+
127
+
128
+ CONDA_NO_GPU = """
129
+ FROM jhnnsrs/tensorflow_conda:latest
130
+
131
+ # setup conda virtual environment
132
+ COPY ./requirements.yml /tmp/requirements.yml
133
+ RUN conda env create --name camera-seg -f /tmp/requirements.yml
134
+
135
+ RUN conda activate camera-seg
136
+ """
137
+
138
+ BUILDERS = {
139
+ "pip": {
140
+ "gpu": None,
141
+ "no-gpu": pip_no_gpu,
142
+ },
143
+ "conda": None,
144
+ "poetry": {
145
+ "gpu": None,
146
+ "no-gpu": poetry_no_gpu,
147
+ },
148
+ }
149
+
150
+
151
+ def build_dockerfile(
152
+ manifest: Manifest, packager: Packager, gpu: bool, python_version: str
153
+ ) -> str:
154
+ """A function to build a dockerfile
155
+
156
+ It delegates to the correct builder, based on the packager and gpu support
157
+
158
+
159
+ Parameters
160
+ ----------
161
+ manifest : Manifest
162
+ The manifest of the project
163
+ packager : Packager
164
+ The detected packager
165
+ gpu : bool
166
+ The detected gpu support
167
+ python_version : str
168
+ _description_
169
+
170
+ Returns
171
+ -------
172
+ str
173
+ The dockerfile
174
+
175
+ Raises
176
+ ------
177
+ click.ClickException
178
+ An exception if the packager is not supported
179
+ """
180
+
181
+ strategy = BUILDERS.get(packager, {})
182
+
183
+ if not strategy:
184
+ raise click.ClickException(
185
+ f"Packager {packager} is not supported. Please create a issue on github and create your own Dockerfile for now."
186
+ )
187
+
188
+ builder = strategy.get(gpu and "gpu" or "no-gpu", None)
189
+
190
+ if not builder:
191
+ raise click.ClickException(
192
+ f"Packager {packager} {gpu and 'with GPU Support'} is not supported. Please create a issue on github and create your own Dockerfile for now."
193
+ )
194
+
195
+ return builder(manifest, python_version)
196
+
197
+
198
+ def docker_file_wizard(manifest: Manifest, auto: bool = True) -> str:
199
+ """A wizard to generate a dockerfile
200
+
201
+ Parameters
202
+ ----------
203
+ manifest : Manifest
204
+ The manifest of the project
205
+ auto : bool, optional
206
+ Should we autodefault prompts to true, by default True
207
+
208
+ Returns
209
+ -------
210
+ str
211
+ The dockerfile
212
+
213
+ ------
214
+ ImportError
215
+ If toml is not installed
216
+ """
217
+ python_version = (
218
+ f"{ sys.version_info.major}.{ sys.version_info.minor}.{ sys.version_info.micro}"
219
+ )
220
+ python_version = (
221
+ python_version
222
+ if auto
223
+ else click.prompt(
224
+ "Which python version to do you want to use?", default=python_version
225
+ )
226
+ )
227
+
228
+ packager = None
229
+
230
+ gpu = "gpu" in manifest.requirements
231
+
232
+ # loading environment
233
+ # check if running in conda
234
+ conda_env = os.environ.get("CONDA_DEFAULT_ENV")
235
+ if conda_env:
236
+ click.echo(f"Found running in conda environment {conda_env}")
237
+
238
+ packager = Packager.CONDA
239
+
240
+ # Check if pyproject.toml exists
241
+ if os.path.exists("pyproject.toml"):
242
+ # Check if poetry is installed
243
+ try:
244
+ import toml
245
+ except ImportError as e:
246
+ raise ImportError("Please install toml to use this feature") from e
247
+
248
+ # Check if poetry is used
249
+ pyproject = toml.load("pyproject.toml")
250
+ if "tool" in pyproject and "poetry" in pyproject["tool"]:
251
+ click.echo("Found poetry project")
252
+ do = True if auto else click.confirm("Do you want to use poetry?")
253
+ if do:
254
+ packager = Packager.POETRY
255
+
256
+ if not packager:
257
+ click.echo(
258
+ "No advanced packager found. Considering using poetry "
259
+ "with a pyproject.toml or conda with a environment.yml"
260
+ )
261
+
262
+ if os.path.exists("requirements.txt"):
263
+ click.prompt("Found requirements.txt file in current directory. Using it.")
264
+
265
+ packager = Packager.PIP
266
+
267
+ if not click.confirm(
268
+ f"Would you like to generate Template Dockerfile: Using {packager} with Python {python_version} {'and' if gpu else 'without'} GPU support"
269
+ ):
270
+ raise click.Abort("Aborted")
271
+
272
+ dockfile = build_dockerfile(
273
+ manifest, packager, gpu=gpu, python_version=python_version
274
+ )
275
+
276
+ return dockfile
277
+
278
+
279
+ def check_overwrite_dockerfile(ctx: Context, param: str, value: bool) -> bool:
280
+ """A callback to check if the dockerfile should be overwritten
281
+
282
+ Parameters
283
+ ----------
284
+ ctx : Context
285
+ The context of the click command
286
+ param : str
287
+ The parameter name
288
+ value : str
289
+ The value of the parameter
290
+
291
+ Returns
292
+ -------
293
+ bool
294
+ Should we overwrite the dockerfile?
295
+ """
296
+
297
+ app_file = ctx.params["dockerfile"]
298
+ if os.path.exists(app_file) and not value:
299
+ should_overwrite = click.confirm(
300
+ "Docker already exists. Do you want to overwrite?", abort=True
301
+ )
302
+ if should_overwrite:
303
+ return True
304
+ else:
305
+ raise click.Abort()
306
+
307
+ return value
308
+
309
+
310
+ @click.command()
311
+ @click.option("--dockerfile", help="The dockerfile to generate", default="Dockerfile")
312
+ @click.option(
313
+ "--overwrite-dockerfile",
314
+ "-o",
315
+ help="Should we overwrite the existing Dockerfile?",
316
+ is_flag=True,
317
+ default=False,
318
+ callback=check_overwrite_dockerfile,
319
+ )
320
+ @click.pass_context
321
+ def wizard(ctx: Context, dockerfile: str, overwrite_dockerfile: bool) -> None:
322
+ """Runs the port wizard to generate a dockerfile to be used with port"""
323
+
324
+ manifest = get_manifest(ctx)
325
+
326
+ dockfile = docker_file_wizard(manifest)
327
+
328
+ with open(dockerfile, "w") as file:
329
+ file.write(dockfile)
File without changes
@@ -0,0 +1,349 @@
1
+ from importlib import import_module, reload
2
+ import asyncio
3
+ from arkitekt_next import App
4
+ from watchfiles import awatch, Change
5
+ from rich.panel import Panel
6
+ from rich.console import Console
7
+ from watchfiles.filters import PythonFilter
8
+ import os
9
+ import sys
10
+ import inspect
11
+ from rekuest.definition.registry import get_default_definition_registry
12
+ from rekuest.agents.hooks import get_default_hook_registry
13
+ from typing import MutableSet, Tuple, Any, Set
14
+ from arkitekt_next.cli.ui import construct_changes_group, construct_app_group
15
+ from arkitekt_next.cli.commands.run.utils import import_builder
16
+ from arkitekt_next.cli.types import Manifest
17
+ from arkitekt_next.apps.types import App
18
+ import rich_click as click
19
+ import os
20
+ from arkitekt_next.cli.options import (
21
+ with_fakts_url,
22
+ with_builder,
23
+ with_token,
24
+ with_instance_id,
25
+ with_headless,
26
+ with_log_level,
27
+ with_skip_cache,
28
+ )
29
+ from arkitekt_next.cli.vars import get_console, get_manifest
30
+
31
+
32
+ class EntrypointFilter(PythonFilter):
33
+ """Checks if the entrypoint is changed"""
34
+
35
+ def __init__(self, entrypoint: str, *args, **kwargs) -> None:
36
+ """A filter that checks if the entrypoint is changed
37
+
38
+ Parameters
39
+ ----------
40
+ entrypoint : str
41
+ The entrypoint to check
42
+ """
43
+ super().__init__(*args, **kwargs)
44
+ self.entrypoint = entrypoint
45
+
46
+ def __call__(self, change: Change, path: str) -> bool:
47
+ """Checks if any of the python filters are changed
48
+ _description_
49
+ Parameters
50
+ ----------
51
+ change : Change
52
+ The change type
53
+ path : str
54
+ The causing path
55
+
56
+ Returns
57
+ -------
58
+ bool
59
+ Should we reload?
60
+ """
61
+ x = super().__call__(change, path)
62
+ if not x:
63
+ return False
64
+
65
+ basename = os.path.basename(path)
66
+ module_name = basename.split(".")[0]
67
+
68
+ return module_name == self.entrypoint
69
+
70
+
71
+ class DeepFilter(PythonFilter):
72
+ """Checks if any of the python filters are changed"""
73
+
74
+ def __call__(self, change: Change, path: str) -> bool:
75
+ """Checks if any of the python filters are changed
76
+
77
+ Parameters
78
+ ----------
79
+ change : Change
80
+ The change type
81
+ path : str
82
+ The causing path
83
+
84
+ Returns
85
+ -------
86
+ bool
87
+ Should we reload?
88
+ """
89
+ return super().__call__(change, path)
90
+
91
+
92
+ async def run_app(app: App) -> None:
93
+ """A helper function to run the app
94
+
95
+ Parameters
96
+ ----------
97
+ app : App
98
+ The app to run
99
+
100
+ """
101
+
102
+ rekuest = app.services.get("rekuest")
103
+ async with app:
104
+ await rekuest.run()
105
+
106
+
107
+ def reload_modules(reloadable_modules) -> None:
108
+ """Reloads the modules in the reloadable_modules set"""
109
+ for module in reloadable_modules:
110
+ reload(sys.modules[module])
111
+
112
+
113
+ def check_deeps(changes: Set[Tuple[Change, str]]) -> Set[str]:
114
+ """Checks if any of the changes
115
+ are happening in a module that is installed
116
+ and returns the modules that should be reloaded
117
+
118
+
119
+
120
+ Parameters
121
+ ----------
122
+ changes : Set[ Tuple[Change, str] ]
123
+ The changes to check
124
+
125
+ Returns
126
+ -------
127
+ Set[str]
128
+ A set of modules that should be reloaded
129
+ """
130
+ normalized = [os.path.normpath(file) for modified, file in changes]
131
+
132
+ reloadable_modules = set()
133
+
134
+ for key, v in sys.modules.items():
135
+ try:
136
+ filepath = inspect.getfile(v)
137
+ except OSError:
138
+ continue
139
+ except TypeError:
140
+ continue
141
+
142
+ for i in normalized:
143
+ if filepath.startswith(i):
144
+ reloadable_modules.add(key)
145
+
146
+ return reloadable_modules
147
+
148
+
149
+ def reset_structure() -> None:
150
+ """Resets the default defintiion rgistry and all
151
+ regitered nodes"""
152
+ get_default_definition_registry().definitions.clear()
153
+ get_default_hook_registry().reset()
154
+
155
+
156
+ def is_entrypoint_change(
157
+ changes: MutableSet[Tuple[Any, str]], entrypoint_real_path: str
158
+ ) -> bool:
159
+ for change, path in changes:
160
+ if os.path.normpath(path) == entrypoint_real_path:
161
+ return True
162
+ return False
163
+
164
+
165
+ async def run_dev(
166
+ console: Console,
167
+ manifest: Manifest,
168
+ version=None,
169
+ builder: str = "arkitekt_next.builders.easy",
170
+ deep: bool = False,
171
+ **builder_kwargs,
172
+ ):
173
+ entrypoint = manifest.entrypoint
174
+ identifier = manifest.identifier
175
+ version = version or "dev"
176
+ entrypoint_file = f"{manifest.entrypoint}.py"
177
+ os.path.realpath(entrypoint_file)
178
+
179
+ builder_func = import_builder(builder)
180
+
181
+ generation_message = "[not bold white]This is a development tool for arkitekt_next apps. It will watch your app for changes and reload it when it detects a change. It will also print out the current state of your app.[/]"
182
+
183
+ if deep:
184
+ generation_message += "\n\n - [not bold white][b]Deep mode[/] is enabled. This will watch all your installed packages for changes and reload them if they are changed.[/]"
185
+ else:
186
+ generation_message += "\n\n - [not bold white][b]Deep mode[/] is disabled. This will only watch your entrypoint for changes.[/]"
187
+
188
+ panel = Panel(
189
+ generation_message,
190
+ style="bold green",
191
+ border_style="green",
192
+ title="ArkitektNext Dev Mode",
193
+ )
194
+ console.print(panel)
195
+
196
+ try:
197
+ module = import_module(manifest.entrypoint)
198
+
199
+ except Exception:
200
+ console.print_exception()
201
+ panel = Panel(
202
+ f"Error while importing your entrypoint please fix your file {entrypoint} and save",
203
+ style="bold red",
204
+ border_style="red",
205
+ )
206
+ console.print(panel)
207
+ module = None
208
+
209
+ def callback(future):
210
+ if future.cancelled():
211
+ return
212
+ else:
213
+ has_exception = future.exception()
214
+
215
+ if not has_exception:
216
+ panel = Panel(
217
+ "App finished running", style="bold yellow", border_style="yellow"
218
+ )
219
+ console.print(panel)
220
+ else:
221
+ try:
222
+ raise has_exception
223
+ except Exception:
224
+ console.print_exception()
225
+ panel = Panel(
226
+ "Error running App", style="bold red", border_style="red"
227
+ )
228
+ console.print(panel)
229
+
230
+ try:
231
+ app: App = builder_func(
232
+ identifier=identifier,
233
+ version=version,
234
+ logo=manifest.logo,
235
+ **builder_kwargs,
236
+ )
237
+ group = construct_app_group(app)
238
+ panel = Panel(group, style="bold green", border_style="green")
239
+ console.print(panel)
240
+
241
+ x = asyncio.create_task(run_app(app))
242
+ x.add_done_callback(callback)
243
+ except Exception:
244
+ console.print_exception()
245
+ panel = Panel(
246
+ "Error building initial App", style="bold red", border_style="red"
247
+ )
248
+ console.print(panel)
249
+
250
+ async for changes in awatch(
251
+ ".",
252
+ watch_filter=EntrypointFilter(entrypoint) if not deep else DeepFilter(),
253
+ debounce=2000,
254
+ step=500,
255
+ ):
256
+ if deep:
257
+ to_be_reloaded = check_deeps(changes)
258
+ if not to_be_reloaded:
259
+ continue
260
+
261
+ group = construct_changes_group(changes)
262
+ panel = Panel(group, style="bold blue", border_style="blue")
263
+ console.print(panel)
264
+
265
+ console.print(panel)
266
+ # Cancelling the app
267
+ if not x or x.done():
268
+ pass
269
+
270
+ else:
271
+ x.cancel()
272
+ panel = Panel(
273
+ "Cancelling latest version", style="bold yellow", border_style="yellow"
274
+ )
275
+ console.print(panel)
276
+ try:
277
+ await x
278
+
279
+ except asyncio.CancelledError:
280
+ pass
281
+
282
+ # Restarting the app
283
+ try:
284
+ with console.status("Reloading module..."):
285
+ reset_structure()
286
+
287
+ if not module:
288
+ module = import_module(entrypoint)
289
+ else:
290
+ if deep:
291
+ reload_modules(to_be_reloaded)
292
+ else:
293
+ reload(module)
294
+ except Exception:
295
+ console.print_exception()
296
+ panel = Panel(
297
+ "Reload unsucessfull please fix your app and save",
298
+ style="bold red",
299
+ border_style="red",
300
+ )
301
+ console.print(panel)
302
+ continue
303
+
304
+ try:
305
+ app = builder_func(
306
+ identifier=identifier,
307
+ version=version,
308
+ logo=manifest.logo,
309
+ **builder_kwargs,
310
+ )
311
+ group = construct_app_group(app)
312
+ panel = Panel(group, style="bold green", border_style="green")
313
+ console.print(panel)
314
+
315
+ x = asyncio.create_task(run_app(app))
316
+ x.add_done_callback(callback)
317
+ except Exception:
318
+ console.print_exception()
319
+ panel = Panel(
320
+ "Error building reloaded App", style="bold red", border_style="red"
321
+ )
322
+ console.print(panel)
323
+
324
+
325
+ @click.command()
326
+ @with_fakts_url
327
+ @with_builder
328
+ @with_token
329
+ @with_instance_id
330
+ @with_headless
331
+ @with_log_level
332
+ @with_skip_cache
333
+ @click.option(
334
+ "--deep",
335
+ help="Should we check the whole directory for changes and reload them when changes?",
336
+ is_flag=True,
337
+ )
338
+ @click.pass_context
339
+ def dev(ctx, **kwargs):
340
+ """Runs the app in dev mode (with hot reloading)
341
+
342
+ Running the app in dev mode will automatically reload the app when changes are detected.
343
+ This is useful for development and debugging.
344
+ """
345
+
346
+ manifest = get_manifest(ctx)
347
+ console = get_console(ctx)
348
+
349
+ asyncio.run(run_dev(console, manifest, **kwargs))
@@ -0,0 +1,22 @@
1
+ import rich_click as click
2
+ from .dev import dev
3
+ from .prod import prod
4
+
5
+
6
+ @click.group()
7
+ @click.pass_context
8
+ def run(ctx):
9
+ """Runs your arkitekt_next app
10
+
11
+ Running your app locally is the first step to developing your app. You can run your app in
12
+ development mode, which will automatically reload your app when you change the code, or in
13
+ production mode, which not reload your app when you change the code, but allows you to
14
+ scale your app to multiple processes.
15
+
16
+
17
+
18
+ """
19
+
20
+
21
+ run.add_command(dev, "dev")
22
+ run.add_command(prod, "prod")