vortex-cli 3.0.0__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.
- vortex/__init__.py +0 -0
- vortex/__main__.py +6 -0
- vortex/colour.py +18 -0
- vortex/commands/__init__.py +0 -0
- vortex/commands/clean.py +21 -0
- vortex/commands/clone.py +219 -0
- vortex/commands/code.py +22 -0
- vortex/commands/config.py +34 -0
- vortex/commands/copy.py +158 -0
- vortex/commands/delete.py +84 -0
- vortex/commands/find.py +46 -0
- vortex/commands/grep.py +68 -0
- vortex/commands/list.py +72 -0
- vortex/commands/log.py +32 -0
- vortex/commands/new.py +126 -0
- vortex/commands/watch.py +339 -0
- vortex/constants.py +6 -0
- vortex/main.py +483 -0
- vortex/models.py +529 -0
- vortex/puakma/lib/XmlSchema-1.4.2.jar +0 -0
- vortex/puakma/lib/activation-1.1.jar +0 -0
- vortex/puakma/lib/annogen-0.1.0.jar +0 -0
- vortex/puakma/lib/axiom-api-1.2.7.jar +0 -0
- vortex/puakma/lib/axiom-dom-1.2.7.jar +0 -0
- vortex/puakma/lib/axiom-impl-1.2.7.jar +0 -0
- vortex/puakma/lib/axis2-adb-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-adb-codegen-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-ant-plugin-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-clustering-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-codegen-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-corba-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-fastinfoset-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-java2wsdl-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-jaxbri-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-jaxws-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-jaxws-api-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-jibx-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-json-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-jws-api-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-kernel-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-metadata-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-mtompolicy-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-saaj-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-saaj-api-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-spring-1.4.1.jar +0 -0
- vortex/puakma/lib/axis2-xmlbeans-1.4.1.jar +0 -0
- vortex/puakma/lib/backport-util-concurrent-3.1.jar +0 -0
- vortex/puakma/lib/bcprov-jdk14-131.jar +0 -0
- vortex/puakma/lib/commons-codec-1.10.jar +0 -0
- vortex/puakma/lib/commons-codec-1.3.jar +0 -0
- vortex/puakma/lib/commons-collections4-4.1.jar +0 -0
- vortex/puakma/lib/commons-fileupload-1.2.jar +0 -0
- vortex/puakma/lib/commons-httpclient-3.1.jar +0 -0
- vortex/puakma/lib/commons-io-1.4.jar +0 -0
- vortex/puakma/lib/commons-logging-1.1.1.jar +0 -0
- vortex/puakma/lib/commons-logging-1.2.jar +0 -0
- vortex/puakma/lib/curvesapi-1.04.jar +0 -0
- vortex/puakma/lib/dom4j-1.6.1.jar +0 -0
- vortex/puakma/lib/geronimo-annotation_1.0_spec-1.1.jar +0 -0
- vortex/puakma/lib/geronimo-stax-api_1.0_spec-1.0.1.jar +0 -0
- vortex/puakma/lib/httpcore-4.0-beta1.jar +0 -0
- vortex/puakma/lib/httpcore-nio-4.0-beta1.jar +0 -0
- vortex/puakma/lib/iText-rtf-2.1.3.jar +0 -0
- vortex/puakma/lib/itext-2.1.7.jar +0 -0
- vortex/puakma/lib/itext-pdfa-5.5.1-javadoc.jar +0 -0
- vortex/puakma/lib/itext-pdfa-5.5.1-sources.jar +0 -0
- vortex/puakma/lib/itext-pdfa-5.5.1.jar +0 -0
- vortex/puakma/lib/itext-xtra-5.5.1-javadoc.jar +0 -0
- vortex/puakma/lib/itext-xtra-5.5.1-sources.jar +0 -0
- vortex/puakma/lib/itext-xtra-5.5.1.jar +0 -0
- vortex/puakma/lib/itextpdf-5.5.1-javadoc.jar +0 -0
- vortex/puakma/lib/itextpdf-5.5.1-sources.jar +0 -0
- vortex/puakma/lib/itextpdf-5.5.1.jar +0 -0
- vortex/puakma/lib/jalopy-1.5rc3.jar +0 -0
- vortex/puakma/lib/jaxb-api-2.1.jar +0 -0
- vortex/puakma/lib/jaxb-impl-2.1.6.jar +0 -0
- vortex/puakma/lib/jaxb-xjc-2.1.6.jar +0 -0
- vortex/puakma/lib/jaxen-1.1.1.jar +0 -0
- vortex/puakma/lib/jcifs-1.2.13.jar +0 -0
- vortex/puakma/lib/jcifs-1.3.3.jar +0 -0
- vortex/puakma/lib/jcifs-ext-0.9.4.jar +0 -0
- vortex/puakma/lib/jcommon-1.0.12.jar +0 -0
- vortex/puakma/lib/jettison-1.0-RC2.jar +0 -0
- vortex/puakma/lib/jfreechart-1.0.13.jar +0 -0
- vortex/puakma/lib/jfreechart-1.0.17.jar +0 -0
- vortex/puakma/lib/jibx-bind-1.1.5.jar +0 -0
- vortex/puakma/lib/jibx-run-1.1.5.jar +0 -0
- vortex/puakma/lib/json-20101028.jar +0 -0
- vortex/puakma/lib/json-20110712.jar +0 -0
- vortex/puakma/lib/junit-4.12.jar +0 -0
- vortex/puakma/lib/log4j-1.2.15.jar +0 -0
- vortex/puakma/lib/log4j-1.2.17.jar +0 -0
- vortex/puakma/lib/mail-1.4.jar +0 -0
- vortex/puakma/lib/mex-1.4.1.jar +0 -0
- vortex/puakma/lib/neethi-2.0.4.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-examples-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-excelant-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-ooxml-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-ooxml-schemas-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.10.1/poi-scratchpad-3.10.1-20140818.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-3.17.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-examples-3.17.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-excelant-3.17.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-ooxml-3.17.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-ooxml-schemas-3.17.jar +0 -0
- vortex/puakma/lib/poi_3.17/poi-scratchpad-3.17.jar +0 -0
- vortex/puakma/lib/shaj-0.5.jar +0 -0
- vortex/puakma/lib/soapmonitor-1.4.1.jar +0 -0
- vortex/puakma/lib/stax-api-1.0.1.jar +0 -0
- vortex/puakma/lib/woden-api-1.0M8.jar +0 -0
- vortex/puakma/lib/woden-impl-dom-1.0M8.jar +0 -0
- vortex/puakma/lib/wsdl4j-1.6.2.jar +0 -0
- vortex/puakma/lib/wstx-asl-3.2.4.jar +0 -0
- vortex/puakma/lib/xalan-2.7.0.jar +0 -0
- vortex/puakma/lib/xercesImpl-2.8.1.jar +0 -0
- vortex/puakma/lib/xml-apis-1.3.04.jar +0 -0
- vortex/puakma/lib/xml-resolver-1.2.jar +0 -0
- vortex/puakma/lib/xmlbeans-2.3.0.jar +0 -0
- vortex/puakma/lib/xmlbeans-2.6.0.jar +0 -0
- vortex/puakma/lib-int/puakma.jar +0 -0
- vortex/puakma/lib-int/puakma.vortex.wst_1.1.0.jar +0 -0
- vortex/puakma/lib-int/retroweaver-rt-1.2.4.jar +0 -0
- vortex/puakma/lib-int/velocity-1.5.jar +0 -0
- vortex/soap.py +332 -0
- vortex/spinner.py +53 -0
- vortex/util.py +192 -0
- vortex/workspace.py +271 -0
- vortex_cli-3.0.0.dist-info/LICENSE +21 -0
- vortex_cli-3.0.0.dist-info/METADATA +179 -0
- vortex_cli-3.0.0.dist-info/RECORD +134 -0
- vortex_cli-3.0.0.dist-info/WHEEL +5 -0
- vortex_cli-3.0.0.dist-info/entry_points.txt +2 -0
- vortex_cli-3.0.0.dist-info/top_level.txt +1 -0
vortex/commands/list.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import tabulate
|
|
6
|
+
|
|
7
|
+
from vortex import util
|
|
8
|
+
from vortex.models import PuakmaApplication
|
|
9
|
+
from vortex.models import PuakmaServer
|
|
10
|
+
from vortex.workspace import Workspace
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("vortex")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def list_(
|
|
16
|
+
workspace: Workspace,
|
|
17
|
+
server: PuakmaServer,
|
|
18
|
+
*,
|
|
19
|
+
group_filter: list[str],
|
|
20
|
+
name_filter: list[str],
|
|
21
|
+
template_filter: list[str],
|
|
22
|
+
show_ids_only: bool = False,
|
|
23
|
+
show_inherited: bool = False,
|
|
24
|
+
show_inactive: bool = False,
|
|
25
|
+
show_local_only: bool = False,
|
|
26
|
+
open_urls: bool = False,
|
|
27
|
+
open_dev_urls: bool = False,
|
|
28
|
+
) -> int:
|
|
29
|
+
if show_local_only:
|
|
30
|
+
apps = workspace.listapps()
|
|
31
|
+
else:
|
|
32
|
+
with server as s:
|
|
33
|
+
apps = s.fetch_all_apps(
|
|
34
|
+
name_filter,
|
|
35
|
+
group_filter,
|
|
36
|
+
template_filter,
|
|
37
|
+
show_inherited,
|
|
38
|
+
show_inactive,
|
|
39
|
+
)
|
|
40
|
+
if open_urls or open_dev_urls:
|
|
41
|
+
util.open_app_urls(*apps, open_dev_url=open_dev_urls)
|
|
42
|
+
else:
|
|
43
|
+
if show_ids_only:
|
|
44
|
+
for app in apps:
|
|
45
|
+
print(app.id)
|
|
46
|
+
else:
|
|
47
|
+
_render_app_list(apps, show_inherited=show_inherited)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _render_app_list(
|
|
52
|
+
apps: list[PuakmaApplication],
|
|
53
|
+
*,
|
|
54
|
+
show_inherited: bool,
|
|
55
|
+
) -> None:
|
|
56
|
+
row_headers = [
|
|
57
|
+
"ID",
|
|
58
|
+
"Name",
|
|
59
|
+
"Group",
|
|
60
|
+
"Template Name",
|
|
61
|
+
]
|
|
62
|
+
row_data = []
|
|
63
|
+
|
|
64
|
+
if show_inherited:
|
|
65
|
+
row_headers.append("Inherits From")
|
|
66
|
+
|
|
67
|
+
for app in sorted(apps, key=lambda x: (x.group.casefold(), x.name.casefold())):
|
|
68
|
+
row = [app.id, app.name, app.group, app.template_name]
|
|
69
|
+
if show_inherited:
|
|
70
|
+
row.append(app.inherit_from)
|
|
71
|
+
row_data.append(row)
|
|
72
|
+
print(tabulate.tabulate(row_data, headers=row_headers))
|
vortex/commands/log.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import tabulate
|
|
6
|
+
|
|
7
|
+
from vortex import util
|
|
8
|
+
from vortex.models import PuakmaServer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def log(server: PuakmaServer, limit: int) -> int:
|
|
12
|
+
with server as s:
|
|
13
|
+
logs = s.get_last_log_items(limit)
|
|
14
|
+
|
|
15
|
+
row_headers = ("Time", "Type", "Source", "Message")
|
|
16
|
+
row_data = []
|
|
17
|
+
for log in sorted(logs, key=lambda x: (x.id)):
|
|
18
|
+
row = [
|
|
19
|
+
datetime.strftime(log.date, "%H:%M:%S"),
|
|
20
|
+
log.type,
|
|
21
|
+
util.shorten_text(log.item_source),
|
|
22
|
+
log.msg,
|
|
23
|
+
]
|
|
24
|
+
row_data.append(row)
|
|
25
|
+
|
|
26
|
+
print(
|
|
27
|
+
tabulate.tabulate(
|
|
28
|
+
row_data, headers=row_headers, maxcolwidths=[None, None, 30, 80]
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return 0
|
vortex/commands/new.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
|
|
8
|
+
from vortex.models import DesignObject
|
|
9
|
+
from vortex.models import DesignObjectAmbiguousError
|
|
10
|
+
from vortex.models import DesignObjectNotFound
|
|
11
|
+
from vortex.models import DesignType
|
|
12
|
+
from vortex.models import PuakmaServer
|
|
13
|
+
from vortex.soap import AppDesigner
|
|
14
|
+
from vortex.util import render_objects
|
|
15
|
+
from vortex.workspace import Workspace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("vortex")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def new(
|
|
22
|
+
workspace: Workspace,
|
|
23
|
+
server: PuakmaServer,
|
|
24
|
+
names: list[str],
|
|
25
|
+
app_id: int,
|
|
26
|
+
design_type: DesignType,
|
|
27
|
+
content_type: str | None = None,
|
|
28
|
+
comment: str = "",
|
|
29
|
+
inherit_from: str = "",
|
|
30
|
+
parent_page: str | None = None,
|
|
31
|
+
open_action: str | None = None,
|
|
32
|
+
save_action: str | None = None,
|
|
33
|
+
) -> int:
|
|
34
|
+
apps = [app for app in workspace.listapps(server) if app.id == app_id]
|
|
35
|
+
if not apps:
|
|
36
|
+
logger.error(f"No cloned application found with ID {app_id}")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
if design_type.is_java_type:
|
|
40
|
+
content_type = "application/java"
|
|
41
|
+
elif design_type == DesignType.PAGE:
|
|
42
|
+
content_type = "text/html"
|
|
43
|
+
_ext = mimetypes.guess_extension(content_type) if content_type else None
|
|
44
|
+
if _ext is None or content_type is None:
|
|
45
|
+
logger.error(f"Unable to determine file type for '{content_type}'")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
app = apps.pop()
|
|
49
|
+
objs: list[DesignObject] = []
|
|
50
|
+
for name in names:
|
|
51
|
+
try:
|
|
52
|
+
_, _obj = app.lookup_design_obj(name)
|
|
53
|
+
except DesignObjectNotFound:
|
|
54
|
+
# OK
|
|
55
|
+
pass
|
|
56
|
+
except DesignObjectAmbiguousError as e:
|
|
57
|
+
logger.error(e)
|
|
58
|
+
return 1
|
|
59
|
+
else:
|
|
60
|
+
logger.error(f"Design Object {_obj} already exists in {app}")
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
design_source = base64.b64encode(design_type.source_template(name)).decode()
|
|
64
|
+
objs.append(
|
|
65
|
+
DesignObject(
|
|
66
|
+
-1,
|
|
67
|
+
name,
|
|
68
|
+
app,
|
|
69
|
+
design_type,
|
|
70
|
+
content_type,
|
|
71
|
+
"",
|
|
72
|
+
design_source,
|
|
73
|
+
comment=comment,
|
|
74
|
+
inherit_from=inherit_from,
|
|
75
|
+
parent_page=parent_page,
|
|
76
|
+
open_action=open_action,
|
|
77
|
+
save_action=save_action,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
if not objs:
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
_show_params = True if parent_page or open_action or save_action else False
|
|
84
|
+
print("The following Design Objects will be created:\n")
|
|
85
|
+
render_objects(objs, show_params=_show_params)
|
|
86
|
+
if input("\n[Y/y] to continue:") not in ["Y", "y"]:
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
with workspace.exclusive_lock():
|
|
90
|
+
ret = asyncio.run(_acreate_objects(workspace, server, objs))
|
|
91
|
+
workspace.mkdir(app)
|
|
92
|
+
return ret
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _acreate_objects(
|
|
96
|
+
workspace: Workspace, server: PuakmaServer, objs: list[DesignObject]
|
|
97
|
+
) -> int:
|
|
98
|
+
async with server as s:
|
|
99
|
+
await s.server_designer.ainitiate_connection()
|
|
100
|
+
logger.info(f"Connected to {s.host}")
|
|
101
|
+
tasks = []
|
|
102
|
+
for obj in objs:
|
|
103
|
+
task = asyncio.create_task(_acreate(workspace, s.app_designer, obj))
|
|
104
|
+
tasks.append(task)
|
|
105
|
+
ret = 0
|
|
106
|
+
for done in asyncio.as_completed(tasks):
|
|
107
|
+
try:
|
|
108
|
+
ret |= await done
|
|
109
|
+
except (asyncio.CancelledError, Exception) as e:
|
|
110
|
+
for task in tasks:
|
|
111
|
+
task.cancel()
|
|
112
|
+
raise e
|
|
113
|
+
return ret
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def _acreate(
|
|
117
|
+
workspace: Workspace,
|
|
118
|
+
app_designer: AppDesigner,
|
|
119
|
+
obj: DesignObject,
|
|
120
|
+
) -> int:
|
|
121
|
+
ok = await obj.acreate(app_designer)
|
|
122
|
+
if ok:
|
|
123
|
+
await obj.acreate_params(app_designer)
|
|
124
|
+
obj.app.design_objects.append(obj)
|
|
125
|
+
await asyncio.to_thread(obj.save, workspace)
|
|
126
|
+
return 0 if ok else 1
|
vortex/commands/watch.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import mimetypes
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from watchfiles import awatch
|
|
11
|
+
from watchfiles import BaseFilter
|
|
12
|
+
from watchfiles import Change
|
|
13
|
+
|
|
14
|
+
from vortex.colour import Colour
|
|
15
|
+
from vortex.models import DesignObject
|
|
16
|
+
from vortex.models import DesignObjectAmbiguousError
|
|
17
|
+
from vortex.models import DesignObjectNotFound
|
|
18
|
+
from vortex.models import DesignPath
|
|
19
|
+
from vortex.models import DesignType
|
|
20
|
+
from vortex.models import InvalidDesignPathError
|
|
21
|
+
from vortex.models import JavaClassVersion
|
|
22
|
+
from vortex.models import PuakmaApplication
|
|
23
|
+
from vortex.models import PuakmaServer
|
|
24
|
+
from vortex.spinner import Spinner
|
|
25
|
+
from vortex.workspace import Workspace
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("vortex")
|
|
28
|
+
|
|
29
|
+
_CLASS_EXT = ".class"
|
|
30
|
+
|
|
31
|
+
WorkspaceChange = tuple[Change, DesignPath, PuakmaApplication]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _error(change: Change, name: str, msg: str) -> Literal[False]:
|
|
35
|
+
msg = f"Failed to process '{name}': {msg}"
|
|
36
|
+
logger.warning(f"({change.raw_str()}) {msg}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _WorkspaceFilter(BaseFilter):
|
|
41
|
+
ignore_files: tuple[str, ...] = (
|
|
42
|
+
".DS_Store",
|
|
43
|
+
PuakmaApplication.PICKLE_FILE,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(self, workspace: Workspace, server: PuakmaServer) -> None:
|
|
47
|
+
self.workspace = workspace
|
|
48
|
+
self.server = server
|
|
49
|
+
|
|
50
|
+
def __call__(self, change: Change, _path: str) -> bool:
|
|
51
|
+
def _err(msg: str) -> bool:
|
|
52
|
+
return _error(change, path.name, msg)
|
|
53
|
+
|
|
54
|
+
path = Path(_path)
|
|
55
|
+
_is_class_file = path.suffix == _CLASS_EXT
|
|
56
|
+
|
|
57
|
+
do_event = (change in (Change.modified, Change.added) and path.is_file()) or (
|
|
58
|
+
change == Change.deleted and not _is_class_file
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not do_event or path.name in self.ignore_files:
|
|
62
|
+
return False
|
|
63
|
+
try:
|
|
64
|
+
design_path = DesignPath(self.workspace, path)
|
|
65
|
+
except InvalidDesignPathError as e:
|
|
66
|
+
return _err(str(e))
|
|
67
|
+
else:
|
|
68
|
+
design_server_host = design_path.app.host
|
|
69
|
+
if design_server_host != self.server.host:
|
|
70
|
+
msg = f"({design_server_host}) does not match ({self.server.host})"
|
|
71
|
+
return _err(msg)
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WorkspaceWatcher:
|
|
76
|
+
def __init__(
|
|
77
|
+
self, workspace: Workspace, server: PuakmaServer, spinner: Spinner | None = None
|
|
78
|
+
) -> None:
|
|
79
|
+
self.workspace = workspace
|
|
80
|
+
self.server = server
|
|
81
|
+
self.spinner = spinner
|
|
82
|
+
self.filter = _WorkspaceFilter(workspace, server)
|
|
83
|
+
self._func_dispatch = {
|
|
84
|
+
Change.modified: self._update_design,
|
|
85
|
+
Change.added: self._create_design,
|
|
86
|
+
Change.deleted: self._delete_design,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async def watch(self) -> int:
|
|
90
|
+
async with self.server as s:
|
|
91
|
+
msg = await s.server_designer.ainitiate_connection()
|
|
92
|
+
logger.info(msg)
|
|
93
|
+
|
|
94
|
+
_error = False
|
|
95
|
+
changes = None
|
|
96
|
+
dirs = self.workspace.listdir(s, strict=True)
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
try:
|
|
100
|
+
async for changes in awatch(*dirs, watch_filter=self.filter):
|
|
101
|
+
try:
|
|
102
|
+
await self._handle_changes(changes)
|
|
103
|
+
except (Exception, asyncio.CancelledError) as e:
|
|
104
|
+
logger.critical(e, stack_info=True)
|
|
105
|
+
_error = True
|
|
106
|
+
break
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error getting changes: {e}")
|
|
109
|
+
if changes:
|
|
110
|
+
try:
|
|
111
|
+
await self._handle_changes(changes)
|
|
112
|
+
except (Exception, asyncio.CancelledError) as e:
|
|
113
|
+
logger.critical(e, stack_info=True)
|
|
114
|
+
_error = True
|
|
115
|
+
if _error:
|
|
116
|
+
break
|
|
117
|
+
return 1 if _error else 0
|
|
118
|
+
|
|
119
|
+
async def _handle_changes(self, changes: set[tuple[Change, str]]) -> None:
|
|
120
|
+
apps: dict[int, PuakmaApplication] = {}
|
|
121
|
+
modified: list[WorkspaceChange] = []
|
|
122
|
+
added_or_deleted: list[WorkspaceChange] = []
|
|
123
|
+
|
|
124
|
+
for change, _path in changes:
|
|
125
|
+
path = Path(_path)
|
|
126
|
+
_is_class_file = path.suffix == _CLASS_EXT
|
|
127
|
+
if _is_class_file and change == Change.added:
|
|
128
|
+
change = Change.modified
|
|
129
|
+
design_path = DesignPath(self.workspace, path)
|
|
130
|
+
|
|
131
|
+
app_id = design_path.app.id
|
|
132
|
+
if app_id not in apps.keys():
|
|
133
|
+
apps[app_id] = design_path.app
|
|
134
|
+
app = apps[app_id]
|
|
135
|
+
val = change, design_path, app
|
|
136
|
+
|
|
137
|
+
if change == Change.modified:
|
|
138
|
+
modified.append(val)
|
|
139
|
+
elif change in [Change.added, Change.deleted]:
|
|
140
|
+
added_or_deleted.append(val)
|
|
141
|
+
|
|
142
|
+
if modified:
|
|
143
|
+
await self._dispatch_modified(modified)
|
|
144
|
+
if added_or_deleted:
|
|
145
|
+
await self._dispatch_added_deleted(added_or_deleted)
|
|
146
|
+
# now update the apps since design objects will have changed
|
|
147
|
+
for app in apps.values():
|
|
148
|
+
self.workspace.mkdir(app)
|
|
149
|
+
|
|
150
|
+
async def _dispatch_modified(self, changes: list[WorkspaceChange]) -> None:
|
|
151
|
+
tasks = [asyncio.create_task(self._update_design(*args)) for args in changes]
|
|
152
|
+
for result in asyncio.as_completed(tasks):
|
|
153
|
+
try:
|
|
154
|
+
await result
|
|
155
|
+
except asyncio.CancelledError as e:
|
|
156
|
+
logger.error(f"Operation Cancelled. {e}")
|
|
157
|
+
|
|
158
|
+
async def _dispatch_added_deleted(self, changes: list[WorkspaceChange]) -> None:
|
|
159
|
+
# Do this synchronously since we will be prompting the user for confirmation
|
|
160
|
+
for args in changes:
|
|
161
|
+
change, design_path, _ = args
|
|
162
|
+
task = asyncio.create_task(self._func_dispatch[change](*args))
|
|
163
|
+
try:
|
|
164
|
+
await task
|
|
165
|
+
except asyncio.CancelledError as e:
|
|
166
|
+
_error(change, design_path.fname, f"Operation Cancelled. {e}")
|
|
167
|
+
|
|
168
|
+
async def _update_design(
|
|
169
|
+
self, change: Change, design_path: DesignPath, app: PuakmaApplication
|
|
170
|
+
) -> bool:
|
|
171
|
+
def _err_result(msg: str) -> Literal[False]:
|
|
172
|
+
return _error(change, design_path.fname, msg)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
file_bytes = await asyncio.to_thread(design_path.path.read_bytes)
|
|
176
|
+
except OSError as e:
|
|
177
|
+
return _err_result(e.strerror)
|
|
178
|
+
# If we are uploading a class file, lets verify
|
|
179
|
+
# if it has been compiled correctly
|
|
180
|
+
if design_path.ext == _CLASS_EXT:
|
|
181
|
+
is_valid, msg = _validate_java_class_file(
|
|
182
|
+
file_bytes, app.java_class_version
|
|
183
|
+
)
|
|
184
|
+
if not is_valid:
|
|
185
|
+
return _err_result(msg)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
indx, obj = app.lookup_design_obj(design_path.design_name)
|
|
189
|
+
except (DesignObjectNotFound, DesignObjectAmbiguousError) as e:
|
|
190
|
+
return _err_result(str(e))
|
|
191
|
+
|
|
192
|
+
upload_source = obj.design_type.is_java_type and design_path.ext == ".java"
|
|
193
|
+
if upload_source:
|
|
194
|
+
obj.design_source = file_bytes
|
|
195
|
+
else:
|
|
196
|
+
obj.design_data = file_bytes
|
|
197
|
+
|
|
198
|
+
ok = await obj.aupload(self.server.download_designer, upload_source)
|
|
199
|
+
if ok:
|
|
200
|
+
app.design_objects[indx] = obj
|
|
201
|
+
return ok
|
|
202
|
+
|
|
203
|
+
async def _create_design(
|
|
204
|
+
self, change: Change, design_path: DesignPath, app: PuakmaApplication
|
|
205
|
+
) -> bool:
|
|
206
|
+
def _err_result(msg: str) -> Literal[False]:
|
|
207
|
+
return _error(change, design_path.fname, msg)
|
|
208
|
+
|
|
209
|
+
name = design_path.design_name
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
_, _obj = app.lookup_design_obj(design_path.design_name)
|
|
213
|
+
except DesignObjectAmbiguousError as e:
|
|
214
|
+
return _err_result(str(e))
|
|
215
|
+
except DesignObjectNotFound:
|
|
216
|
+
# OK
|
|
217
|
+
pass
|
|
218
|
+
else:
|
|
219
|
+
return _err_result(f"Design Object {_obj} already exists in {app}")
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
design_type = DesignType.from_name(design_path.design_dir)
|
|
223
|
+
except ValueError:
|
|
224
|
+
return _err_result(f"Invalid Design Type '{design_path.design_dir}'")
|
|
225
|
+
|
|
226
|
+
content_type, _ = mimetypes.guess_type(design_path)
|
|
227
|
+
if design_type.is_java_type:
|
|
228
|
+
content_type = "application/java"
|
|
229
|
+
elif design_type == DesignType.PAGE:
|
|
230
|
+
content_type = "text/html"
|
|
231
|
+
if content_type is None:
|
|
232
|
+
return _err_result("Unable to determine content type")
|
|
233
|
+
|
|
234
|
+
obj = DesignObject(-1, name, app, design_type, content_type, "", "")
|
|
235
|
+
|
|
236
|
+
upload_source = obj.design_type.is_java_type
|
|
237
|
+
try:
|
|
238
|
+
file_bytes = design_path.path.read_bytes()
|
|
239
|
+
except OSError as e:
|
|
240
|
+
file_bytes = b""
|
|
241
|
+
msg = f"Couldn't set the design contents from '{design_path}': {e}"
|
|
242
|
+
logger.warning(msg)
|
|
243
|
+
else:
|
|
244
|
+
if upload_source:
|
|
245
|
+
obj.design_source = file_bytes
|
|
246
|
+
else:
|
|
247
|
+
obj.design_data = file_bytes
|
|
248
|
+
|
|
249
|
+
if not self._confirm_change(change, obj):
|
|
250
|
+
return _err_result("Operation Cancelled")
|
|
251
|
+
|
|
252
|
+
create_ok = await obj.acreate(self.server.app_designer)
|
|
253
|
+
if create_ok:
|
|
254
|
+
app.design_objects.append(obj)
|
|
255
|
+
|
|
256
|
+
upload_design_ok = True
|
|
257
|
+
if len(file_bytes) > 0:
|
|
258
|
+
upload_design_ok = await obj.aupload(
|
|
259
|
+
self.server.download_designer, upload_source
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return upload_design_ok and create_ok
|
|
263
|
+
|
|
264
|
+
async def _delete_design(
|
|
265
|
+
self, change: Change, design_path: DesignPath, app: PuakmaApplication
|
|
266
|
+
) -> bool:
|
|
267
|
+
def _err_result(msg: str) -> Literal[False]:
|
|
268
|
+
return _error(change, design_path.fname, msg)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
_, obj = app.lookup_design_obj(design_path.design_name)
|
|
272
|
+
except (DesignObjectNotFound, DesignObjectAmbiguousError) as e:
|
|
273
|
+
return _err_result(str(e))
|
|
274
|
+
|
|
275
|
+
if not self._confirm_change(change, obj):
|
|
276
|
+
return _err_result("Operation Cancelled")
|
|
277
|
+
|
|
278
|
+
await obj.adelete(self.server.app_designer)
|
|
279
|
+
app.design_objects.remove(obj)
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
def _confirm_change(self, change: Change, obj: DesignObject) -> bool:
|
|
283
|
+
action = {
|
|
284
|
+
change.deleted: Colour.highlight("delete", Colour.RED),
|
|
285
|
+
change.modified: "update",
|
|
286
|
+
change.added: "create",
|
|
287
|
+
}[change]
|
|
288
|
+
|
|
289
|
+
if self.spinner and self.spinner.running:
|
|
290
|
+
self.spinner.stop()
|
|
291
|
+
try:
|
|
292
|
+
sys.stdout.write("\n")
|
|
293
|
+
prompt = (
|
|
294
|
+
f"Are you sure you wish to {action} {obj.design_type.name} {obj}? "
|
|
295
|
+
"[Y/y] to confirm:"
|
|
296
|
+
)
|
|
297
|
+
sys.stdout.write(prompt)
|
|
298
|
+
sys.stdout.flush()
|
|
299
|
+
res = sys.stdin.readline().strip()
|
|
300
|
+
except (KeyboardInterrupt, Exception) as e:
|
|
301
|
+
raise asyncio.CancelledError(e)
|
|
302
|
+
else:
|
|
303
|
+
return res in ["Y", "y"]
|
|
304
|
+
finally:
|
|
305
|
+
sys.stdout.write("\n")
|
|
306
|
+
sys.stdout.flush()
|
|
307
|
+
if self.spinner and not self.spinner.running:
|
|
308
|
+
self.spinner.start()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _validate_java_class_file(
|
|
312
|
+
class_file_bytes: bytes, expected_version: JavaClassVersion | None = None
|
|
313
|
+
) -> tuple[bool, str]:
|
|
314
|
+
# https://en.wikipedia.org/wiki/Java_class_file#General_layout
|
|
315
|
+
bytes_header = class_file_bytes[:8]
|
|
316
|
+
if bytes_header[:4] != b"\xca\xfe\xba\xbe":
|
|
317
|
+
return False, "Not a valid Java Class File"
|
|
318
|
+
major_version = int.from_bytes(bytes_header[6:8], byteorder="big")
|
|
319
|
+
minor_version = int.from_bytes(bytes_header[4:6], byteorder="big")
|
|
320
|
+
compiled_version: JavaClassVersion = (major_version, minor_version)
|
|
321
|
+
if expected_version and compiled_version != expected_version:
|
|
322
|
+
return (
|
|
323
|
+
False,
|
|
324
|
+
f"File has been compiled with Java Class Version"
|
|
325
|
+
f"{compiled_version} but expected {expected_version}",
|
|
326
|
+
)
|
|
327
|
+
return True, ""
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def watch(workspace: Workspace, server: PuakmaServer) -> int:
|
|
331
|
+
if not workspace.listdir():
|
|
332
|
+
logger.error(f"No application directories to watch in workspace '{workspace}'")
|
|
333
|
+
return 1
|
|
334
|
+
with (
|
|
335
|
+
workspace.exclusive_lock(),
|
|
336
|
+
Spinner("Watching workspace, ^C to stop") as spinner,
|
|
337
|
+
):
|
|
338
|
+
watcher = WorkspaceWatcher(workspace, server, spinner)
|
|
339
|
+
return asyncio.run(watcher.watch())
|