Skip to content

Commit a00c105

Browse files
authored
Merge branch 'main' into chore/bump-version
2 parents ac988a7 + 1ce2903 commit a00c105

7 files changed

Lines changed: 138 additions & 23 deletions

File tree

.github/workflows/docs.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Deploy docs
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
pages: write
9+
id-token: write
10+
11+
# Nur ein Build gleichzeitig; bei mehreren manuellen Triggern wird der
12+
# laufende Build abgebrochen und der jüngste deployed.
13+
concurrency:
14+
group: pages
15+
cancel-in-progress: true
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Install uv
24+
uses: astral-sh/setup-uv@v5
25+
with:
26+
enable-cache: true
27+
28+
- name: Install docs dependencies
29+
run: uv sync --group docs
30+
31+
- name: Build site
32+
run: uv run mkdocs build --strict
33+
34+
- uses: actions/upload-pages-artifact@v3
35+
with:
36+
path: site
37+
38+
deploy:
39+
needs: build
40+
runs-on: ubuntu-latest
41+
environment:
42+
name: github-pages
43+
url: ${{ steps.deployment.outputs.page_url }}
44+
steps:
45+
- id: deployment
46+
uses: actions/deploy-pages@v4

.github/workflows/publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
runs-on: ubuntu-latest
3838
environment:
3939
name: pypi
40-
url: https://pypi.org/p/viur-core
40+
url: https://pypi.org/p/viur-cli
4141
permissions:
4242
id-token: write # IMPORTANT: mandatory for trusted publishing
4343

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.idea
22
.vscode
33
*.pyc
4+
*.egg-info/

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,4 @@ In case you have appropriate permissions, a release can be done this way:
8484

8585
## Maintenance
8686

87-
Maintainer of this project is [@grashalmbeisser](https://github.com/Grashalmbeisser).
87+
Maintainer of this project is [@KadirBalku](https://github.com/KadirBalku).

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright © 2025 by Mausbrand Informationssysteme GmbH.
1+
Copyright © 2026 by Mausbrand Informationssysteme GmbH.
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy
44
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ viur-cli depends on
323323

324324
## License
325325

326-
Copyright © 2025 by Mausbrand Informationssysteme GmbH.<br>
326+
Copyright © 2026 by Mausbrand Informationssysteme GmbH.<br>
327327
Mausbrand and ViUR are registered trademarks of Mausbrand Informationssysteme GmbH.
328328

329329
This project is free software under the MIT license.<br>

src/viur_cli/scriptor/cli.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import requests
55
import os
66
import hashlib
7+
import difflib
78
import asyncio
89
import sys
910
import glob
@@ -23,7 +24,7 @@ def get_modules():
2324
global _modules
2425
if _modules is None:
2526
_modules = Modules(scriptor_config["base_url"], cookies=scriptor_config["cookies"])
26-
asyncio.new_event_loop().run_until_complete(_modules.init())
27+
asyncio.run(_modules.init())
2728

2829
return _modules
2930

@@ -36,22 +37,39 @@ def script():
3637

3738

3839
@script.command()
39-
@click.option('--url', default=None, help='Set the url')
40+
@click.option('--url', default=None, help='Set the server url')
4041
@click.option('--username', default=None, help='Set the username')
4142
@click.option('--working_dir', default=None, help='Set the working directory where scripts are stored to')
4243
def configure(url: str, username: str, working_dir: str):
4344
"""
4445
Manage configuration settings.
4546
"""
47+
if not any([url, username, working_dir]):
48+
click.echo("No parameters provided. Use one or more of the following options:")
49+
click.echo(" --url Set the server URL")
50+
click.echo(" --username Set the username")
51+
click.echo(" --working_dir Set the working directory where scripts are stored to")
52+
return
53+
54+
changed = []
4655

4756
if url:
4857
scriptor_config["base_url"] = url
58+
changed.append(f"url = {url}")
4959

5060
if username:
5161
scriptor_config["username"] = username
62+
changed.append(f"username = {username}")
5263

5364
if working_dir:
5465
scriptor_config["working_dir"] = working_dir.replace("\\", "/")
66+
changed.append(f"working_dir = {working_dir}")
67+
68+
click.echo("Configuration updated:")
69+
for entry in changed:
70+
click.echo(f" {entry}")
71+
72+
scriptor_config.save()
5573

5674

5775
@script.command()
@@ -111,7 +129,8 @@ def check_session(ctx: click.Context):
111129
get_modules()
112130

113131
@script.command()
114-
@click.option('--force', default=False, help='Force replace files from server in local working directory')
132+
@click.option('--force', default=False, is_flag=True,
133+
help='Overwrite local files without asking for confirmation')
115134
@click.pass_context
116135
def pull(ctx: click.Context, force: bool):
117136
"""
@@ -125,33 +144,74 @@ async def main():
125144
tree = await modules.get_module("script")
126145
working_dir = scriptor_config.get("working_dir")
127146

147+
stats = {"new": 0, "updated": 0, "skipped": 0, "unchanged": 0, "dirs": 0}
148+
128149
async def process_entry(entry: dict, is_node: bool):
129-
_path = os.path.join(working_dir, entry["path"])
150+
_path = os.path.join(working_dir, entry["path"].lstrip("/"))
130151

131152
if is_node:
132153
if not os.path.exists(_path):
154+
click.echo(click.style(f" mkdir {entry['path']}", fg="blue"))
133155
os.makedirs(_path)
156+
stats["dirs"] += 1
134157
else:
158+
click.echo(f" check {entry['path']}", nl=False)
159+
135160
def create_file():
136161
with open(_path, "a+") as f:
137162
f.write(entry["script"])
138163

139-
click.echo(f"Pull {_path}")
140-
141164
if os.path.exists(_path):
142165
if force:
166+
with open(_path, "r") as f:
167+
changed = f.read().splitlines() != entry["script"].splitlines()
143168
os.remove(_path)
144169
create_file()
170+
if changed:
171+
click.echo(click.style(" [updated]", fg="yellow"))
172+
stats["updated"] += 1
173+
else:
174+
click.echo(click.style(" [ok]", fg="green"))
175+
stats["unchanged"] += 1
145176
else:
146177
with open(_path, "r") as f:
147-
if hashlib.sha256(entry["script"].encode()).digest() \
148-
!= hashlib.sha256(f.read().encode()).digest():
149-
if click.confirm(f"There is a difference with {entry['path']}. Overwrite?"):
150-
os.remove(_path)
151-
create_file()
152-
178+
local_content = f.read()
179+
remote_content = entry["script"]
180+
diff = list(difflib.unified_diff(
181+
remote_content.splitlines(),
182+
local_content.splitlines(),
183+
fromfile=f"server/{entry['path'].lstrip('/')}",
184+
tofile=f"local/{entry['path'].lstrip('/')}",
185+
lineterm="",
186+
))
187+
if diff:
188+
click.echo(click.style(" [diff]", fg="yellow"))
189+
for line in diff:
190+
if line.startswith("+++") or line.startswith("---"):
191+
click.echo(click.style(line, bold=True))
192+
elif line.startswith("@@"):
193+
click.echo(click.style(line, fg="cyan"))
194+
elif line.startswith("+"):
195+
click.echo(click.style(line, fg="green"))
196+
elif line.startswith("-"):
197+
click.echo(click.style(line, fg="red"))
198+
else:
199+
click.echo(line)
200+
if click.confirm(f"Overwrite local {entry['path']} with remote version?"):
201+
os.remove(_path)
202+
create_file()
203+
stats["updated"] += 1
204+
else:
205+
stats["skipped"] += 1
206+
else:
207+
click.echo(click.style(" [ok]", fg="green"))
208+
stats["unchanged"] += 1
153209
else:
210+
click.echo(click.style(" [new]", fg="green"))
154211
create_file()
212+
stats["new"] += 1
213+
214+
click.echo("Fetching scripts from server...")
155215

156216
# Process nodes first
157217
async for node in tree.list(skel_type="node"):
@@ -161,7 +221,14 @@ def create_file():
161221
async for leaf in tree.list(skel_type="leaf"):
162222
await process_entry(leaf, False)
163223

164-
asyncio.new_event_loop().run_until_complete(main())
224+
click.echo("")
225+
click.echo(click.style("Summary:", bold=True))
226+
click.echo(f" new: {stats['new']}")
227+
click.echo(f" updated: {stats['updated']}")
228+
click.echo(f" skipped: {stats['skipped']}")
229+
click.echo(f" unchanged: {stats['unchanged']}")
230+
231+
asyncio.run(main())
165232

166233

167234
@script.command()
@@ -305,21 +372,22 @@ def watch_loop():
305372

306373
def on_modified(event):
307374
try:
308-
# check for tmp file
309375
if event.src_path.endswith("~"):
310376
return
377+
if not os.path.exists(event.src_path):
378+
return
311379
if event.src_path not in modified_files:
312380
modified_files[event.src_path] = os.path.getmtime(event.src_path)
313381
elif os.path.getmtime(event.src_path) == modified_files[event.src_path]:
314382
return
315383
modified_files[event.src_path] = os.path.getmtime(event.src_path)
316-
asyncio.new_event_loop().run_until_complete(main(event.src_path))
384+
asyncio.run(main(event.src_path))
317385
except Exception as e:
318386
import traceback
319387
print(f"Error: on file {event.src_path} {e}")
320388
traceback.print_exc()
321389

322-
regexes = [r".*\.py"]
390+
regexes = [r".*\.py$"]
323391
ignore_regexes = []
324392
ignore_directories = True
325393
case_sensitive = False
@@ -341,9 +409,9 @@ def on_modified(event):
341409
observer.stop()
342410
observer.join()
343411

344-
asyncio.new_event_loop().run_until_complete(watch_loop())
412+
watch_loop()
345413
return
346-
asyncio.new_event_loop().run_until_complete(main())
414+
asyncio.run(main())
347415

348416

349417
@script.command()
@@ -376,4 +444,4 @@ async def main():
376444

377445
await module.main()
378446

379-
asyncio.new_event_loop().run_until_complete(main())
447+
asyncio.run(main())

0 commit comments

Comments
 (0)