arthexis 0.1.18__py3-none-any.whl → 0.1.19__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 arthexis might be problematic. Click here for more details.
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/METADATA +37 -10
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/RECORD +26 -26
- config/settings.py +1 -5
- core/system.py +125 -0
- core/tasks.py +0 -22
- core/views.py +35 -4
- nodes/admin.py +1 -2
- nodes/models.py +18 -23
- nodes/tests.py +42 -34
- nodes/urls.py +0 -1
- nodes/views.py +2 -15
- ocpp/admin.py +23 -2
- ocpp/models.py +7 -0
- ocpp/store.py +6 -4
- ocpp/tests.py +14 -1
- ocpp/views.py +65 -12
- pages/admin.py +63 -10
- pages/context_processors.py +11 -0
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/tests.py +177 -34
- pages/urls.py +2 -1
- pages/views.py +70 -23
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/WHEEL +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.19
|
|
4
4
|
Summary: Power & Energy Infrastructure
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -15,7 +15,7 @@ Requires-Dist: amqp==5.3.1
|
|
|
15
15
|
Requires-Dist: annotated-types==0.7.0
|
|
16
16
|
Requires-Dist: anyio==4.9.0
|
|
17
17
|
Requires-Dist: asgiref==3.10.0
|
|
18
|
-
Requires-Dist: atproto==0.0.
|
|
18
|
+
Requires-Dist: atproto==0.0.62
|
|
19
19
|
Requires-Dist: attrs==25.3.0
|
|
20
20
|
Requires-Dist: autobahn==24.4.2
|
|
21
21
|
Requires-Dist: Automat==25.4.16
|
|
@@ -41,7 +41,7 @@ Requires-Dist: defusedxml==0.7.1
|
|
|
41
41
|
Requires-Dist: Django==5.2.7
|
|
42
42
|
Requires-Dist: django-celery-beat==2.8.1
|
|
43
43
|
Requires-Dist: django-debug-toolbar==6.0.0
|
|
44
|
-
Requires-Dist: django-import-export==4.3.
|
|
44
|
+
Requires-Dist: django-import-export==4.3.12
|
|
45
45
|
Requires-Dist: django-object-actions==5.0.0
|
|
46
46
|
Requires-Dist: django-otp==1.5.4
|
|
47
47
|
Requires-Dist: django-timezone-field==7.1
|
|
@@ -53,28 +53,28 @@ Requires-Dist: h11==0.16.0
|
|
|
53
53
|
Requires-Dist: httpcore==1.0.9
|
|
54
54
|
Requires-Dist: httpx==0.28.1
|
|
55
55
|
Requires-Dist: hyperlink==21.0.0
|
|
56
|
-
Requires-Dist: idna==3.
|
|
56
|
+
Requires-Dist: idna==3.11
|
|
57
57
|
Requires-Dist: incremental==24.7.2
|
|
58
58
|
Requires-Dist: kombu==5.5.4
|
|
59
59
|
Requires-Dist: libipld==3.1.1
|
|
60
60
|
Requires-Dist: Markdown==3.8.2
|
|
61
61
|
Requires-Dist: mdx_truly_sane_lists==1.3
|
|
62
|
-
Requires-Dist: mcp==1.
|
|
62
|
+
Requires-Dist: mcp==1.18.0
|
|
63
63
|
Requires-Dist: mfrc522==0.0.7; sys_platform == "linux"
|
|
64
64
|
Requires-Dist: outcome==1.3.0.post0
|
|
65
65
|
Requires-Dist: packaging==25.0
|
|
66
66
|
Requires-Dist: pillow==11.3.0
|
|
67
67
|
Requires-Dist: prompt_toolkit==3.0.51
|
|
68
|
-
Requires-Dist: psutil==
|
|
68
|
+
Requires-Dist: psutil==7.1.1
|
|
69
69
|
Requires-Dist: psycopg==3.2.9
|
|
70
|
-
Requires-Dist: psycopg-binary==3.2.
|
|
70
|
+
Requires-Dist: psycopg-binary==3.2.11
|
|
71
71
|
Requires-Dist: pyasn1==0.6.1
|
|
72
72
|
Requires-Dist: pyasn1_modules==0.4.2
|
|
73
73
|
Requires-Dist: pycparser==2.22
|
|
74
74
|
Requires-Dist: pydantic==2.11.7
|
|
75
75
|
Requires-Dist: pydantic_core==2.33.2
|
|
76
76
|
Requires-Dist: pyOpenSSL==25.1.0
|
|
77
|
-
Requires-Dist: pyperclip==1.
|
|
77
|
+
Requires-Dist: pyperclip==1.11.0
|
|
78
78
|
Requires-Dist: PySocks==1.7.1
|
|
79
79
|
Requires-Dist: python-crontab==3.3.0
|
|
80
80
|
Requires-Dist: python-dateutil==2.9.0.post0
|
|
@@ -99,7 +99,7 @@ Requires-Dist: trio-websocket==0.12.2
|
|
|
99
99
|
Requires-Dist: Twisted==25.5.0
|
|
100
100
|
Requires-Dist: twine==6.1.0
|
|
101
101
|
Requires-Dist: txaio==25.6.1
|
|
102
|
-
Requires-Dist: typing-inspection==0.4.
|
|
102
|
+
Requires-Dist: typing-inspection==0.4.2
|
|
103
103
|
Requires-Dist: typing_extensions==4.14.1
|
|
104
104
|
Requires-Dist: tzdata==2025.2
|
|
105
105
|
Requires-Dist: urllib3==2.5.0
|
|
@@ -108,7 +108,7 @@ Requires-Dist: wcwidth==0.2.13
|
|
|
108
108
|
Requires-Dist: webencodings==0.5.1
|
|
109
109
|
Requires-Dist: websocket-client==1.8.0
|
|
110
110
|
Requires-Dist: websockets==13.1
|
|
111
|
-
Requires-Dist: whitenoise==6.
|
|
111
|
+
Requires-Dist: whitenoise==6.11.0
|
|
112
112
|
Requires-Dist: plyer==2.1.0; sys_platform == "win32"
|
|
113
113
|
Requires-Dist: wsproto==1.2.0
|
|
114
114
|
Requires-Dist: zope.interface==7.2
|
|
@@ -212,10 +212,37 @@ Terminal nodes can start directly with the scripts below without installing; Con
|
|
|
212
212
|
### 4. Administration
|
|
213
213
|
Visit [`http://localhost:8000/admin/`](http://localhost:8000/admin/) for the [Django admin](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) and [`http://localhost:8000/admindocs/`](http://localhost:8000/admindocs/) for the [admindocs](https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/). Use `--port` with the start scripts or installer when you need to expose a different port.
|
|
214
214
|
|
|
215
|
+
## Sigils
|
|
216
|
+
|
|
217
|
+
Sigils are bracketed tokens such as `[ENV.SMTP_PASSWORD]` that Arthexis expands at runtime. They make it possible to reference configuration secrets, system metadata, or records stored in other apps without duplicating values across the project.
|
|
218
|
+
|
|
219
|
+
### Syntax at a glance
|
|
220
|
+
|
|
221
|
+
- `[PREFIX.KEY]` — returns a field or attribute. Hyphens and casing are normalized automatically.
|
|
222
|
+
- `[PREFIX=IDENTIFIER.FIELD]` — selects a specific record by primary key or any unique field.
|
|
223
|
+
- `[PREFIX:FIELD=VALUE.ATTRIBUTE]` — filters by a custom field instead of the primary key.
|
|
224
|
+
- `[PREFIX.FIELD=[OTHER.SIGIL]]` — nests sigils so the value after `=` resolves before the outer token.
|
|
225
|
+
- `[PREFIX]` — for entity prefixes, returns the serialized object in JSON; for configuration prefixes, resolves to an empty string when the key is missing.
|
|
226
|
+
|
|
227
|
+
The platform ships with three configuration prefixes:
|
|
228
|
+
|
|
229
|
+
- `ENV` reads environment variables.
|
|
230
|
+
- `CONF` reads Django settings.
|
|
231
|
+
- `SYS` exposes computed system information such as build metadata.
|
|
232
|
+
|
|
233
|
+
Additional prefixes are defined through **Sigil Roots**, which map a short code (for example `ROLE`, `ODOO`, or `USER`) to a Django model. You can review them from **Admin → Sigil Builder** (`/admin/sigil-builder/`), where a test console is also available.
|
|
234
|
+
|
|
235
|
+
Unknown prefixes remain in place (e.g. `[UNKNOWN.VALUE]`) and are logged. When the optional `gway` CLI is installed, the resolver will attempt to delegate unresolved tokens to it before falling back to the original text.
|
|
236
|
+
|
|
215
237
|
## Support
|
|
216
238
|
|
|
217
239
|
Contact us at [tecnologia@gelectriic.com](mailto:tecnologia@gelectriic.com) or visit our [web page](https://www.gelectriic.com/) for [professional services](https://en.wikipedia.org/wiki/Professional_services) and [commercial support](https://en.wikipedia.org/wiki/Technical_support).
|
|
218
240
|
|
|
241
|
+
## Project Guidelines
|
|
242
|
+
|
|
243
|
+
- [AGENTS](AGENTS.md) – operating handbook for repository workflows, testing, and release management.
|
|
244
|
+
- [DESIGN](DESIGN.md) – visual, UX, and branding guidance that all interfaces must follow.
|
|
245
|
+
|
|
219
246
|
## About Me
|
|
220
247
|
|
|
221
248
|
> "What, you want to know about me too? Well, I enjoy [developing software](https://en.wikipedia.org/wiki/Software_development), [role-playing games](https://en.wikipedia.org/wiki/Role-playing_game), long walks on the [beach](https://en.wikipedia.org/wiki/Beach) and a fourth secret thing."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
|
|
3
3
|
config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
|
|
4
4
|
config/asgi.py,sha256=T-0QSbtieEWKPIDkEcEdd-q6qjK8ZCwwjCaISOBkWdM,1296
|
|
@@ -10,7 +10,7 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
|
|
|
10
10
|
config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
|
|
11
11
|
config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
|
|
12
12
|
config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
|
|
13
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=620NySqqkpp-U-hDRyHp5M-U_JBweqvyxi4FUJ2rc_M,21454
|
|
14
14
|
config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
|
|
15
15
|
config/urls.py,sha256=zvU4FSMKPlXUrGDjUgJCRFQztWb78wo1urW2DQf8qdI,5463
|
|
16
16
|
config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
|
|
@@ -43,70 +43,70 @@ core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,35
|
|
|
43
43
|
core/sigil_builder.py,sha256=VLwbrrD7Zr3SHfIDYV-V7uv7LEGiIelCSkeGswHibuc,4843
|
|
44
44
|
core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
|
-
core/system.py,sha256=
|
|
47
|
-
core/tasks.py,sha256=
|
|
46
|
+
core/system.py,sha256=RyBqooWezM0Li_KCRskchD5Lub0cdqBmrnU6ilC8MPE,39823
|
|
47
|
+
core/tasks.py,sha256=Vjv3wQaK-eRs3osSTfVbtgzBoBQvTHXHbrOKxDY7V00,11617
|
|
48
48
|
core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
|
|
49
49
|
core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
|
|
50
50
|
core/tests.py,sha256=H0vPRcwcD3HNscRInbMGvdzs9ixvmkLh3d4YSEeQfEc,98679
|
|
51
51
|
core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
|
|
52
52
|
core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
|
|
53
53
|
core/user_data.py,sha256=02CfvxayELWSWZJCxWpv1Yz7EGg08yEu5MM31Khsi0U,21083
|
|
54
|
-
core/views.py,sha256=
|
|
54
|
+
core/views.py,sha256=oH210iFack4Yvt4ug5DGM3mx24GhPGFhZOlzi4WMV_w,89298
|
|
55
55
|
core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
|
|
56
56
|
core/workgroup_urls.py,sha256=XR9IqwsSBI8epW7_-hHhWFU9wsyJfZehHwNQBhCgmpM,407
|
|
57
57
|
core/workgroup_views.py,sha256=vtumF3-8YaTD-K6nSd8eYvUyq3ftpvWSEwtcp5B-P6o,2889
|
|
58
58
|
nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
nodes/admin.py,sha256=
|
|
59
|
+
nodes/admin.py,sha256=QuBB7JMqUvu2io6ZoZVTdqXLOX0CaB-394yUuwiRqDM,62022
|
|
60
60
|
nodes/apps.py,sha256=AxK-sh9JBJZwNOLjqw9omCQGUQWw-45VRdYH07XhVJU,2732
|
|
61
61
|
nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
|
|
62
62
|
nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
|
|
63
63
|
nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
|
|
64
64
|
nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
|
|
65
|
-
nodes/models.py,sha256=
|
|
65
|
+
nodes/models.py,sha256=ay2FE-M0I82SR3__BPdfrXAaOvBL4CKua9_xOOm3mKg,62593
|
|
66
66
|
nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
|
|
67
67
|
nodes/rfid_sync.py,sha256=SP_BRUhgYMBH-zXrcM7uShgFSGYmmuIMb1OdcU1e-5U,6956
|
|
68
68
|
nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
|
|
69
69
|
nodes/tasks.py,sha256=ur59ebu9z02idmvy_IvUQt3eu9LWRyyNpkg2szvIHCQ,1522
|
|
70
|
-
nodes/tests.py,sha256=
|
|
71
|
-
nodes/urls.py,sha256=
|
|
70
|
+
nodes/tests.py,sha256=w_vUQNatk2K-FIu1yd1I4iD95q68DQ0H9uDYx1Cl3gA,156975
|
|
71
|
+
nodes/urls.py,sha256=OaPypFt5gcmVe87NDzt1WMiC3YwHezih4jV_pGkICcE,601
|
|
72
72
|
nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
|
|
73
|
-
nodes/views.py,sha256=
|
|
73
|
+
nodes/views.py,sha256=qJnAW5lIO5Up6n3IiTutERHZJTU-eZ_3ChpuZBIZ3oU,22526
|
|
74
74
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
|
-
ocpp/admin.py,sha256=
|
|
75
|
+
ocpp/admin.py,sha256=2gnFJFvDDTJmmMuuPVf9ZEuKLLsc6Z7_HFYSlg6TlwE,35336
|
|
76
76
|
ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
|
|
77
77
|
ocpp/consumers.py,sha256=s51784IwUoWfLaMI_zfnCve7ouZIB5xAnlgwThb40Gw,67979
|
|
78
78
|
ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
|
|
79
79
|
ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
|
|
80
|
-
ocpp/models.py,sha256=
|
|
80
|
+
ocpp/models.py,sha256=6BG-maPpcalLFoEHcbpecuXqia9mQx2ZATOu2pvUaBU,31846
|
|
81
81
|
ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
|
|
82
82
|
ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
|
|
83
83
|
ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
|
|
84
84
|
ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
85
|
-
ocpp/store.py,sha256=
|
|
85
|
+
ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
|
|
86
86
|
ocpp/tasks.py,sha256=OxIaI4OSLz9AfwLexnXhiBILBimTs3gVrPd197Jguqg,5819
|
|
87
87
|
ocpp/test_export_import.py,sha256=Zp6xUBlRq7XkdKjOs78BhkujNQdklxi4RLxU8c-udWY,4530
|
|
88
88
|
ocpp/test_rfid.py,sha256=hMFQwYDPhwfTW2XdDT5q__gKrL8YPbv7DNNMdwzJ7BQ,39105
|
|
89
|
-
ocpp/tests.py,sha256=
|
|
89
|
+
ocpp/tests.py,sha256=Y9Z4hfeiqRRCFP8utsVadFTLcKBOwTVIQuDJK3dYqas,186492
|
|
90
90
|
ocpp/transactions_io.py,sha256=YnxI-Tv5UFxv0JuFK3XpoqFYP8eRT8sMuDiqkiMHPtU,7387
|
|
91
91
|
ocpp/urls.py,sha256=3T5O5DSwVk4PbhPx5p4D3UseCWvC5xV5HwJLSM6AfA8,1700
|
|
92
|
-
ocpp/views.py,sha256=
|
|
92
|
+
ocpp/views.py,sha256=TRsfkm8VpygNPW7WIK_hQGX5a2ohrKnTcPfTM11WXVc,60141
|
|
93
93
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
|
-
pages/admin.py,sha256=
|
|
94
|
+
pages/admin.py,sha256=E-jcxUI-89gFgqGI38HMSHbfDw1inpSUiRR83utJ0fc,32512
|
|
95
95
|
pages/apps.py,sha256=o8gQP-VdZOk9LXIEo6IDmOSqX3TP8XypBvKGGWLoQ0k,351
|
|
96
96
|
pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
97
|
-
pages/context_processors.py,sha256=
|
|
97
|
+
pages/context_processors.py,sha256=JELk1V5R5yOVIm5h3JfikG1_V6QSUN3LDlKWF8p9zCE,5543
|
|
98
98
|
pages/defaults.py,sha256=l36APPAZO4ub2A8Pp-lQGujKeOVYcyzU6t7-kOk8VoA,522
|
|
99
99
|
pages/forms.py,sha256=T0atqxdNds3IBP8N-9c5-ACf3iR9FzzmhzK4MOa24e8,7058
|
|
100
|
-
pages/middleware.py,sha256=
|
|
101
|
-
pages/models.py,sha256=
|
|
100
|
+
pages/middleware.py,sha256=MYd5Nko4AnFg3orY6MuyvvNg_I6GCIf8mDW8znSOgvQ,7042
|
|
101
|
+
pages/models.py,sha256=Ms_m_tzzstNghN_JOzyfwsbllDBIl_AKnYdTRTJthqM,22173
|
|
102
102
|
pages/module_defaults.py,sha256=R8n6eQDjNRMpO-DW86OFGvyRarju5Bx7Fnb275R_z24,5411
|
|
103
103
|
pages/site_config.py,sha256=f1Me0GFdHeGbIeyMlQNzD2e6hym59YHqbz92U_ppffY,4057
|
|
104
104
|
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
105
|
-
pages/tests.py,sha256=
|
|
106
|
-
pages/urls.py,sha256=
|
|
105
|
+
pages/tests.py,sha256=5rRi1e_mSoS6VGykrz58VZTb428RJRR9zMi3-Pfg3FU,138871
|
|
106
|
+
pages/urls.py,sha256=BbfEl6SbNfyFo2acA8Wze6aHV2kJR-0339FCIOi6cD4,1231
|
|
107
107
|
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
108
|
-
pages/views.py,sha256=
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
111
|
-
arthexis-0.1.
|
|
112
|
-
arthexis-0.1.
|
|
108
|
+
pages/views.py,sha256=hthdoV352cZRru-Wi3Wtv0ZXOjOba7z7rIpHLlzfuVo,47665
|
|
109
|
+
arthexis-0.1.19.dist-info/METADATA,sha256=ko-yFBbCoQEs9EUfs6BzAcFK5qfn_7uvRJcqbk6pgpQ,11757
|
|
110
|
+
arthexis-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
111
|
+
arthexis-0.1.19.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
112
|
+
arthexis-0.1.19.dist-info/RECORD,,
|
config/settings.py
CHANGED
|
@@ -585,7 +585,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|
|
585
585
|
LANGUAGE_CODE = "en-us"
|
|
586
586
|
|
|
587
587
|
LANGUAGES = [
|
|
588
|
-
("es", _("Spanish")),
|
|
588
|
+
("es", _("Spanish (Latin America)")),
|
|
589
589
|
("en", _("English")),
|
|
590
590
|
("it", _("Italian")),
|
|
591
591
|
("de", _("German")),
|
|
@@ -688,8 +688,4 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
688
688
|
"task": "core.tasks.heartbeat",
|
|
689
689
|
"schedule": crontab(minute="*/5"),
|
|
690
690
|
},
|
|
691
|
-
"birthday_greetings": {
|
|
692
|
-
"task": "core.tasks.birthday_greetings",
|
|
693
|
-
"schedule": crontab(hour=9, minute=0),
|
|
694
|
-
},
|
|
695
691
|
}
|
core/system.py
CHANGED
|
@@ -13,8 +13,10 @@ import shutil
|
|
|
13
13
|
import logging
|
|
14
14
|
from typing import Callable, Iterable, Optional
|
|
15
15
|
|
|
16
|
+
from django import forms
|
|
16
17
|
from django.conf import settings
|
|
17
18
|
from django.contrib import admin, messages
|
|
19
|
+
from django.forms import modelformset_factory
|
|
18
20
|
from django.template.response import TemplateResponse
|
|
19
21
|
from django.http import HttpResponseRedirect
|
|
20
22
|
from django.urls import path, reverse
|
|
@@ -32,6 +34,7 @@ from core.release import (
|
|
|
32
34
|
_remote_with_credentials,
|
|
33
35
|
)
|
|
34
36
|
from core.tasks import check_github_updates
|
|
37
|
+
from core.models import Todo
|
|
35
38
|
from utils import revision
|
|
36
39
|
|
|
37
40
|
|
|
@@ -1086,6 +1089,123 @@ def _system_upgrade_report_view(request):
|
|
|
1086
1089
|
return TemplateResponse(request, "admin/system_upgrade_report.html", context)
|
|
1087
1090
|
|
|
1088
1091
|
|
|
1092
|
+
class PendingTodoForm(forms.ModelForm):
|
|
1093
|
+
mark_done = forms.BooleanField(required=False, label=_("Approve"))
|
|
1094
|
+
|
|
1095
|
+
class Meta:
|
|
1096
|
+
model = Todo
|
|
1097
|
+
fields = [
|
|
1098
|
+
"request",
|
|
1099
|
+
"request_details",
|
|
1100
|
+
"url",
|
|
1101
|
+
"generated_for_version",
|
|
1102
|
+
"generated_for_revision",
|
|
1103
|
+
"on_done_condition",
|
|
1104
|
+
]
|
|
1105
|
+
widgets = {
|
|
1106
|
+
"request_details": forms.Textarea(attrs={"rows": 3}),
|
|
1107
|
+
"on_done_condition": forms.Textarea(attrs={"rows": 2}),
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
def __init__(self, *args, **kwargs):
|
|
1111
|
+
super().__init__(*args, **kwargs)
|
|
1112
|
+
for name in [
|
|
1113
|
+
"request",
|
|
1114
|
+
"url",
|
|
1115
|
+
"generated_for_version",
|
|
1116
|
+
"generated_for_revision",
|
|
1117
|
+
]:
|
|
1118
|
+
self.fields[name].widget.attrs.setdefault("class", "vTextField")
|
|
1119
|
+
for name in ["request_details", "on_done_condition"]:
|
|
1120
|
+
self.fields[name].widget.attrs.setdefault("class", "vLargeTextField")
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
PendingTodoFormSet = modelformset_factory(Todo, form=PendingTodoForm, extra=0)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _system_pending_todos_report_view(request):
|
|
1127
|
+
queryset = (
|
|
1128
|
+
Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1129
|
+
.order_by("request")
|
|
1130
|
+
)
|
|
1131
|
+
formset = PendingTodoFormSet(
|
|
1132
|
+
request.POST or None,
|
|
1133
|
+
queryset=queryset,
|
|
1134
|
+
prefix="todos",
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
if request.method == "POST":
|
|
1138
|
+
if formset.is_valid():
|
|
1139
|
+
approved_count = 0
|
|
1140
|
+
edited_count = 0
|
|
1141
|
+
for form in formset.forms:
|
|
1142
|
+
mark_done = form.cleaned_data.get("mark_done")
|
|
1143
|
+
todo = form.save(commit=False)
|
|
1144
|
+
has_changes = form.has_changed()
|
|
1145
|
+
if mark_done and todo.done_on is None:
|
|
1146
|
+
todo.done_on = timezone.now()
|
|
1147
|
+
approved_count += 1
|
|
1148
|
+
has_changes = True
|
|
1149
|
+
if has_changes:
|
|
1150
|
+
todo.save()
|
|
1151
|
+
if form.has_changed():
|
|
1152
|
+
edited_count += 1
|
|
1153
|
+
if has_changes and form.has_changed():
|
|
1154
|
+
form.save_m2m()
|
|
1155
|
+
|
|
1156
|
+
if approved_count or edited_count:
|
|
1157
|
+
message_parts: list[str] = []
|
|
1158
|
+
if edited_count:
|
|
1159
|
+
message_parts.append(
|
|
1160
|
+
ngettext(
|
|
1161
|
+
"%(count)d TODO updated.",
|
|
1162
|
+
"%(count)d TODOs updated.",
|
|
1163
|
+
edited_count,
|
|
1164
|
+
)
|
|
1165
|
+
% {"count": edited_count}
|
|
1166
|
+
)
|
|
1167
|
+
if approved_count:
|
|
1168
|
+
message_parts.append(
|
|
1169
|
+
ngettext(
|
|
1170
|
+
"%(count)d TODO approved.",
|
|
1171
|
+
"%(count)d TODOs approved.",
|
|
1172
|
+
approved_count,
|
|
1173
|
+
)
|
|
1174
|
+
% {"count": approved_count}
|
|
1175
|
+
)
|
|
1176
|
+
messages.success(request, " ".join(message_parts))
|
|
1177
|
+
else:
|
|
1178
|
+
messages.info(
|
|
1179
|
+
request,
|
|
1180
|
+
_("No changes were applied to the pending TODOs."),
|
|
1181
|
+
)
|
|
1182
|
+
return HttpResponseRedirect(reverse("admin:system-pending-todos-report"))
|
|
1183
|
+
else:
|
|
1184
|
+
messages.error(request, _("Please correct the errors below."))
|
|
1185
|
+
|
|
1186
|
+
rows = [
|
|
1187
|
+
{
|
|
1188
|
+
"form": form,
|
|
1189
|
+
"todo": form.instance,
|
|
1190
|
+
}
|
|
1191
|
+
for form in formset.forms
|
|
1192
|
+
]
|
|
1193
|
+
|
|
1194
|
+
context = admin.site.each_context(request)
|
|
1195
|
+
context.update(
|
|
1196
|
+
{
|
|
1197
|
+
"title": _("Pending TODOs Report"),
|
|
1198
|
+
"formset": formset,
|
|
1199
|
+
"rows": rows,
|
|
1200
|
+
}
|
|
1201
|
+
)
|
|
1202
|
+
return TemplateResponse(
|
|
1203
|
+
request,
|
|
1204
|
+
"admin/system_pending_todos_report.html",
|
|
1205
|
+
context,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
|
|
1089
1209
|
def _trigger_upgrade_check() -> bool:
|
|
1090
1210
|
"""Return ``True`` when the upgrade check was queued asynchronously."""
|
|
1091
1211
|
|
|
@@ -1142,6 +1262,11 @@ def patch_admin_system_view() -> None:
|
|
|
1142
1262
|
admin.site.admin_view(_system_changelog_report_view),
|
|
1143
1263
|
name="system-changelog-report",
|
|
1144
1264
|
),
|
|
1265
|
+
path(
|
|
1266
|
+
"system/pending-todos-report/",
|
|
1267
|
+
admin.site.admin_view(_system_pending_todos_report_view),
|
|
1268
|
+
name="system-pending-todos-report",
|
|
1269
|
+
),
|
|
1145
1270
|
path(
|
|
1146
1271
|
"system/upgrade-report/",
|
|
1147
1272
|
admin.site.admin_view(_system_upgrade_report_view),
|
core/tasks.py
CHANGED
|
@@ -8,14 +8,9 @@ import urllib.error
|
|
|
8
8
|
import urllib.request
|
|
9
9
|
|
|
10
10
|
from celery import shared_task
|
|
11
|
-
from django.conf import settings
|
|
12
|
-
from django.contrib.auth import get_user_model
|
|
13
|
-
from core import mailer
|
|
14
11
|
from core import github_issues
|
|
15
12
|
from django.utils import timezone
|
|
16
13
|
|
|
17
|
-
from nodes.models import NetMessage
|
|
18
|
-
|
|
19
14
|
|
|
20
15
|
AUTO_UPGRADE_HEALTH_DELAY_SECONDS = 30
|
|
21
16
|
AUTO_UPGRADE_SKIP_LOCK_NAME = "auto_upgrade_skip_revisions.lck"
|
|
@@ -30,23 +25,6 @@ def heartbeat() -> None:
|
|
|
30
25
|
logger.info("Heartbeat task executed")
|
|
31
26
|
|
|
32
27
|
|
|
33
|
-
@shared_task
|
|
34
|
-
def birthday_greetings() -> None:
|
|
35
|
-
"""Send birthday greetings to users via Net Message and email."""
|
|
36
|
-
User = get_user_model()
|
|
37
|
-
today = timezone.localdate()
|
|
38
|
-
for user in User.objects.filter(birthday=today):
|
|
39
|
-
NetMessage.broadcast("Happy bday!", user.username)
|
|
40
|
-
if user.email:
|
|
41
|
-
mailer.send(
|
|
42
|
-
"Happy bday!",
|
|
43
|
-
f"Happy bday! {user.username}",
|
|
44
|
-
[user.email],
|
|
45
|
-
settings.DEFAULT_FROM_EMAIL,
|
|
46
|
-
fail_silently=True,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
28
|
def _auto_upgrade_log_path(base_dir: Path) -> Path:
|
|
51
29
|
"""Return the log file used for auto-upgrade events."""
|
|
52
30
|
|
core/views.py
CHANGED
|
@@ -736,6 +736,34 @@ def _ensure_release_todo(
|
|
|
736
736
|
return todo, fixture_path
|
|
737
737
|
|
|
738
738
|
|
|
739
|
+
def _todo_blocks_publish(todo: Todo, release: PackageRelease) -> bool:
|
|
740
|
+
"""Return ``True`` when ``todo`` should block the release workflow."""
|
|
741
|
+
|
|
742
|
+
request = (todo.request or "").strip()
|
|
743
|
+
release_name = (release.package.name or "").strip()
|
|
744
|
+
if not request or not release_name:
|
|
745
|
+
return True
|
|
746
|
+
|
|
747
|
+
prefix = f"create release {release_name.lower()} "
|
|
748
|
+
if not request.lower().startswith(prefix):
|
|
749
|
+
return True
|
|
750
|
+
|
|
751
|
+
release_version = (release.version or "").strip()
|
|
752
|
+
generated_version = (todo.generated_for_version or "").strip()
|
|
753
|
+
if not release_version or release_version != generated_version:
|
|
754
|
+
return True
|
|
755
|
+
|
|
756
|
+
generated_revision = (todo.generated_for_revision or "").strip()
|
|
757
|
+
release_revision = (release.revision or "").strip()
|
|
758
|
+
if generated_revision and release_revision and generated_revision != release_revision:
|
|
759
|
+
return True
|
|
760
|
+
|
|
761
|
+
if not todo.is_seed_data:
|
|
762
|
+
return True
|
|
763
|
+
|
|
764
|
+
return False
|
|
765
|
+
|
|
766
|
+
|
|
739
767
|
def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
740
768
|
"""Ensure ``release`` matches the repository revision and version.
|
|
741
769
|
|
|
@@ -1885,12 +1913,15 @@ def release_progress(request, pk: int, action: str):
|
|
|
1885
1913
|
|
|
1886
1914
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1887
1915
|
pending_items = list(pending_qs)
|
|
1888
|
-
|
|
1916
|
+
blocking_todos = [
|
|
1917
|
+
todo for todo in pending_items if _todo_blocks_publish(todo, release)
|
|
1918
|
+
]
|
|
1919
|
+
if not blocking_todos:
|
|
1889
1920
|
ctx["todos_ack"] = True
|
|
1890
1921
|
ctx["todos_ack_auto"] = True
|
|
1891
1922
|
elif ack_todos_requested:
|
|
1892
1923
|
failures = []
|
|
1893
|
-
for todo in
|
|
1924
|
+
for todo in blocking_todos:
|
|
1894
1925
|
result = todo.check_on_done_condition()
|
|
1895
1926
|
if not result.passed:
|
|
1896
1927
|
failures.append((todo, result))
|
|
@@ -1920,7 +1951,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1920
1951
|
"url": todo.url,
|
|
1921
1952
|
"request_details": todo.request_details,
|
|
1922
1953
|
}
|
|
1923
|
-
for todo in
|
|
1954
|
+
for todo in blocking_todos
|
|
1924
1955
|
]
|
|
1925
1956
|
ctx["todos_required"] = True
|
|
1926
1957
|
|
|
@@ -1932,7 +1963,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1932
1963
|
"started": ctx.get("started", False),
|
|
1933
1964
|
}
|
|
1934
1965
|
step_count = 0
|
|
1935
|
-
if not
|
|
1966
|
+
if not blocking_todos:
|
|
1936
1967
|
ctx["todos_ack"] = True
|
|
1937
1968
|
log_path = log_dir / log_name
|
|
1938
1969
|
ctx.setdefault("log", log_name)
|
nodes/admin.py
CHANGED
|
@@ -1565,7 +1565,7 @@ class NetMessageAdmin(EntityModelAdmin):
|
|
|
1565
1565
|
search_fields = ("subject", "body")
|
|
1566
1566
|
list_filter = ("complete", "filter_node_role", "filter_current_relation")
|
|
1567
1567
|
ordering = ("-created",)
|
|
1568
|
-
readonly_fields = ("complete",
|
|
1568
|
+
readonly_fields = ("complete",)
|
|
1569
1569
|
actions = ["send_messages"]
|
|
1570
1570
|
fieldsets = (
|
|
1571
1571
|
(None, {"fields": ("subject", "body")}),
|
|
@@ -1590,7 +1590,6 @@ class NetMessageAdmin(EntityModelAdmin):
|
|
|
1590
1590
|
"node_origin",
|
|
1591
1591
|
"target_limit",
|
|
1592
1592
|
"propagated_to",
|
|
1593
|
-
"confirmed_peers",
|
|
1594
1593
|
"complete",
|
|
1595
1594
|
)
|
|
1596
1595
|
},
|
nodes/models.py
CHANGED
|
@@ -293,7 +293,13 @@ class Node(Entity):
|
|
|
293
293
|
@classmethod
|
|
294
294
|
def register_current(cls):
|
|
295
295
|
"""Create or update the :class:`Node` entry for this host."""
|
|
296
|
-
|
|
296
|
+
hostname_override = (
|
|
297
|
+
os.environ.get("NODE_HOSTNAME")
|
|
298
|
+
or os.environ.get("HOSTNAME")
|
|
299
|
+
or ""
|
|
300
|
+
)
|
|
301
|
+
hostname_override = hostname_override.strip()
|
|
302
|
+
hostname = hostname_override or socket.gethostname()
|
|
297
303
|
try:
|
|
298
304
|
address = socket.gethostbyname(hostname)
|
|
299
305
|
except OSError:
|
|
@@ -305,7 +311,11 @@ class Node(Entity):
|
|
|
305
311
|
rev_value = revision.get_revision()
|
|
306
312
|
installed_revision = rev_value if rev_value else ""
|
|
307
313
|
mac = cls.get_current_mac()
|
|
308
|
-
|
|
314
|
+
endpoint_override = os.environ.get("NODE_PUBLIC_ENDPOINT", "").strip()
|
|
315
|
+
slug_source = endpoint_override or hostname
|
|
316
|
+
slug = slugify(slug_source)
|
|
317
|
+
if not slug:
|
|
318
|
+
slug = cls._generate_unique_public_endpoint(hostname or mac)
|
|
309
319
|
node = cls.objects.filter(mac_address=mac).first()
|
|
310
320
|
if not node:
|
|
311
321
|
node = cls.objects.filter(public_endpoint=slug).first()
|
|
@@ -1410,7 +1420,6 @@ class NetMessage(Entity):
|
|
|
1410
1420
|
propagated_to = models.ManyToManyField(
|
|
1411
1421
|
Node, blank=True, related_name="received_net_messages"
|
|
1412
1422
|
)
|
|
1413
|
-
confirmed_peers = models.JSONField(default=dict, blank=True)
|
|
1414
1423
|
created = models.DateTimeField(auto_now_add=True)
|
|
1415
1424
|
complete = models.BooleanField(default=False, editable=False)
|
|
1416
1425
|
|
|
@@ -1640,10 +1649,7 @@ class NetMessage(Entity):
|
|
|
1640
1649
|
seen_list = seen.copy()
|
|
1641
1650
|
selected_ids = [str(n.uuid) for n in selected]
|
|
1642
1651
|
payload_seen = seen_list + selected_ids
|
|
1643
|
-
confirmed_peers = dict(self.confirmed_peers or {})
|
|
1644
|
-
|
|
1645
1652
|
for node in selected:
|
|
1646
|
-
now = timezone.now().isoformat()
|
|
1647
1653
|
payload = {
|
|
1648
1654
|
"uuid": str(self.uuid),
|
|
1649
1655
|
"subject": self.subject,
|
|
@@ -1679,33 +1685,22 @@ class NetMessage(Entity):
|
|
|
1679
1685
|
headers["X-Signature"] = base64.b64encode(signature).decode()
|
|
1680
1686
|
except Exception:
|
|
1681
1687
|
pass
|
|
1682
|
-
status_entry = {
|
|
1683
|
-
"status": "pending",
|
|
1684
|
-
"status_code": None,
|
|
1685
|
-
"updated": now,
|
|
1686
|
-
}
|
|
1687
1688
|
try:
|
|
1688
|
-
|
|
1689
|
+
requests.post(
|
|
1689
1690
|
f"http://{node.address}:{node.port}/nodes/net-message/",
|
|
1690
1691
|
data=payload_json,
|
|
1691
1692
|
headers=headers,
|
|
1692
1693
|
timeout=1,
|
|
1693
1694
|
)
|
|
1694
|
-
status_entry["status_code"] = getattr(response, "status_code", None)
|
|
1695
|
-
if getattr(response, "ok", False):
|
|
1696
|
-
status_entry["status"] = "acknowledged"
|
|
1697
|
-
else:
|
|
1698
|
-
status_entry["status"] = "failed"
|
|
1699
1695
|
except Exception:
|
|
1700
|
-
|
|
1696
|
+
logger.exception(
|
|
1697
|
+
"Failed to propagate NetMessage %s to node %s",
|
|
1698
|
+
self.pk,
|
|
1699
|
+
node.pk,
|
|
1700
|
+
)
|
|
1701
1701
|
self.propagated_to.add(node)
|
|
1702
|
-
confirmed_peers[str(node.uuid)] = status_entry
|
|
1703
1702
|
|
|
1704
1703
|
save_fields: list[str] = []
|
|
1705
|
-
if confirmed_peers != (self.confirmed_peers or {}):
|
|
1706
|
-
self.confirmed_peers = confirmed_peers
|
|
1707
|
-
save_fields.append("confirmed_peers")
|
|
1708
|
-
|
|
1709
1704
|
if total_known and self.propagated_to.count() >= total_known:
|
|
1710
1705
|
self.complete = True
|
|
1711
1706
|
save_fields.append("complete")
|