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.
- arkitekt_next/__init__.py +43 -0
- arkitekt_next/apps/__init__.py +3 -0
- arkitekt_next/apps/easy.py +99 -0
- arkitekt_next/apps/next.py +40 -0
- arkitekt_next/apps/qt.py +97 -0
- arkitekt_next/apps/service/__init__.py +3 -0
- arkitekt_next/apps/service/fakts.py +88 -0
- arkitekt_next/apps/service/fakts_next.py +79 -0
- arkitekt_next/apps/service/fakts_qt.py +82 -0
- arkitekt_next/apps/service/fluss_next.py +31 -0
- arkitekt_next/apps/service/grant_registry.py +27 -0
- arkitekt_next/apps/service/herre.py +24 -0
- arkitekt_next/apps/service/herre_qt.py +57 -0
- arkitekt_next/apps/service/kabinet.py +31 -0
- arkitekt_next/apps/service/mikro_next.py +81 -0
- arkitekt_next/apps/service/rekuest_next.py +53 -0
- arkitekt_next/apps/service/unlok_next.py +32 -0
- arkitekt_next/apps/types.py +53 -0
- arkitekt_next/builders.py +264 -0
- arkitekt_next/cli/__init__.py +0 -0
- arkitekt_next/cli/commands/call/__init__.py +0 -0
- arkitekt_next/cli/commands/call/local.py +132 -0
- arkitekt_next/cli/commands/call/main.py +22 -0
- arkitekt_next/cli/commands/call/remote.py +90 -0
- arkitekt_next/cli/commands/gen/__init__.py +0 -0
- arkitekt_next/cli/commands/gen/compile.py +45 -0
- arkitekt_next/cli/commands/gen/init.py +122 -0
- arkitekt_next/cli/commands/gen/main.py +29 -0
- arkitekt_next/cli/commands/gen/watch.py +32 -0
- arkitekt_next/cli/commands/init/__init__.py +0 -0
- arkitekt_next/cli/commands/init/main.py +194 -0
- arkitekt_next/cli/commands/inspect/__init__.py +0 -0
- arkitekt_next/cli/commands/inspect/definitions.py +53 -0
- arkitekt_next/cli/commands/inspect/main.py +22 -0
- arkitekt_next/cli/commands/inspect/variables.py +92 -0
- arkitekt_next/cli/commands/manifest/__init__.py +0 -0
- arkitekt_next/cli/commands/manifest/inspect.py +42 -0
- arkitekt_next/cli/commands/manifest/main.py +25 -0
- arkitekt_next/cli/commands/manifest/scopes.py +155 -0
- arkitekt_next/cli/commands/manifest/version.py +147 -0
- arkitekt_next/cli/commands/manifest/wizard.py +94 -0
- arkitekt_next/cli/commands/port/__init__.py +0 -0
- arkitekt_next/cli/commands/port/build.py +231 -0
- arkitekt_next/cli/commands/port/init.py +82 -0
- arkitekt_next/cli/commands/port/main.py +31 -0
- arkitekt_next/cli/commands/port/publish.py +102 -0
- arkitekt_next/cli/commands/port/stage.py +59 -0
- arkitekt_next/cli/commands/port/utils.py +47 -0
- arkitekt_next/cli/commands/port/validate.py +78 -0
- arkitekt_next/cli/commands/port/wizard.py +329 -0
- arkitekt_next/cli/commands/run/__init__.py +0 -0
- arkitekt_next/cli/commands/run/dev.py +349 -0
- arkitekt_next/cli/commands/run/main.py +22 -0
- arkitekt_next/cli/commands/run/prod.py +57 -0
- arkitekt_next/cli/commands/run/utils.py +10 -0
- arkitekt_next/cli/commands/server/__init__.py +0 -0
- arkitekt_next/cli/commands/server/down.py +56 -0
- arkitekt_next/cli/commands/server/init.py +74 -0
- arkitekt_next/cli/commands/server/inspect.py +59 -0
- arkitekt_next/cli/commands/server/main.py +33 -0
- arkitekt_next/cli/commands/server/open.py +66 -0
- arkitekt_next/cli/commands/server/remove.py +60 -0
- arkitekt_next/cli/commands/server/stop.py +56 -0
- arkitekt_next/cli/commands/server/up.py +70 -0
- arkitekt_next/cli/commands/server/utils.py +33 -0
- arkitekt_next/cli/configs/base.yaml +867 -0
- arkitekt_next/cli/constants.py +63 -0
- arkitekt_next/cli/dockerfiles/vanilla.dockerfile +8 -0
- arkitekt_next/cli/errors.py +4 -0
- arkitekt_next/cli/inspect.py +1 -0
- arkitekt_next/cli/io.py +255 -0
- arkitekt_next/cli/main.py +83 -0
- arkitekt_next/cli/options.py +166 -0
- arkitekt_next/cli/schemas/fluss.schema.graphql +2446 -0
- arkitekt_next/cli/schemas/gucker.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/kabinet.schema.graphql +515 -0
- arkitekt_next/cli/schemas/kluster.schema.graphql +109 -0
- arkitekt_next/cli/schemas/konviktion.schema.graphql +70 -0
- arkitekt_next/cli/schemas/kuay.schema.graphql +356 -0
- arkitekt_next/cli/schemas/mikro.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/mikro_next.schema.graphql +1639 -0
- arkitekt_next/cli/schemas/napari.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/omero_ark.schema.graphql +100 -0
- arkitekt_next/cli/schemas/port.schema.graphql +356 -0
- arkitekt_next/cli/schemas/rekuest.schema.graphql +4630 -0
- arkitekt_next/cli/schemas/rekuest_next.schema.graphql +1159 -0
- arkitekt_next/cli/schemas/unlok.schema.graphql +1013 -0
- arkitekt_next/cli/templates/filter.py +26 -0
- arkitekt_next/cli/templates/simple.py +67 -0
- arkitekt_next/cli/texts.py +20 -0
- arkitekt_next/cli/types.py +365 -0
- arkitekt_next/cli/ui.py +111 -0
- arkitekt_next/cli/utils.py +15 -0
- arkitekt_next/cli/validators.py +17 -0
- arkitekt_next/cli/vars.py +39 -0
- arkitekt_next/cli/versions/v1.yaml +1 -0
- arkitekt_next/constants.py +6 -0
- arkitekt_next/model.py +110 -0
- arkitekt_next/qt/__init__.py +9 -0
- arkitekt_next/qt/assets/dark/gear.png +0 -0
- arkitekt_next/qt/assets/dark/green pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/orange pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/pink pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/red pulse.gif +0 -0
- arkitekt_next/qt/assets/light/gear.png +0 -0
- arkitekt_next/qt/assets/light/green pulse.gif +0 -0
- arkitekt_next/qt/assets/light/orange pulse.gif +0 -0
- arkitekt_next/qt/assets/light/pink pulse.gif +0 -0
- arkitekt_next/qt/assets/light/red pulse.gif +0 -0
- arkitekt_next/qt/magic_bar.py +545 -0
- arkitekt_next/qt/utils.py +30 -0
- arkitekt_next/service_registry.py +51 -0
- arkitekt_next/tqdm.py +43 -0
- arkitekt_next/utils.py +38 -0
- arkitekt_next-0.7.8.dist-info/LICENSE +21 -0
- arkitekt_next-0.7.8.dist-info/METADATA +155 -0
- arkitekt_next-0.7.8.dist-info/RECORD +119 -0
- arkitekt_next-0.7.8.dist-info/WHEEL +4 -0
- 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")
|