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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.18
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.61
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.9
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.10
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.16.0
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==5.9.8
68
+ Requires-Dist: psutil==7.1.1
69
69
  Requires-Dist: psycopg==3.2.9
70
- Requires-Dist: psycopg-binary==3.2.9
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.9.0
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.1
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.9.0
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]` &mdash; returns a field or attribute. Hyphens and casing are normalized automatically.
222
+ - `[PREFIX=IDENTIFIER.FIELD]` &mdash; selects a specific record by primary key or any unique field.
223
+ - `[PREFIX:FIELD=VALUE.ATTRIBUTE]` &mdash; filters by a custom field instead of the primary key.
224
+ - `[PREFIX.FIELD=[OTHER.SIGIL]]` &mdash; nests sigils so the value after `=` resolves before the outer token.
225
+ - `[PREFIX]` &mdash; 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 &rarr; 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.18.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
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=fkLL3nbh01KqTVS9M7QH19i3HOuvVD6OTEvApy56Y4w,21569
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=tqx8-4kyViMGKI3EAaxztrbyes4TSTPQ9YsIKzdVs6c,35731
47
- core/tasks.py,sha256=MtijKTtRHUEsTP4nVJFYx5B8Ls8EXmtzpBuq8FU5b9s,12302
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=BTnbaGgSWz4lX7VKVttVIJGWyg7oHnYMMEQTOX44Zhc,88240
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=liGwvusPI3o0RAxhamA8Hs1CMev_DRiFbbNhHWNqIfk,62079
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=1aoifwRm_VHhYCcP4i7P7ZbiDFRLQkTO57FozknWd9A,62840
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=deqjQVAt6sXyI_DdY9zj-Ha3ad1TlnVbhjGhJ-LFqKg,155932
71
- nodes/urls.py,sha256=HmAxj6sr6nMf0lii_1UX7sNBJUcrkaiKm3R9ofUWhvM,677
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=TyW7exkVaR-o2_XkJXSi9jQ_BygXOE2cQFs4xlI20Xc,22905
73
+ nodes/views.py,sha256=qJnAW5lIO5Up6n3IiTutERHZJTU-eZ_3ChpuZBIZ3oU,22526
74
74
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- ocpp/admin.py,sha256=gMxHkT5KSp4kPWJcDJ1Y65VqgrwFTZl8Y516FO8oi3g,34658
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=QjEaygY7Tl47Q6z2uxP6ftUn4JeD8-JQX2fcwrCaEEg,31631
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=rHrP2Iq2ycMFbal1UEJVXb7r4gDtI5yifaE3nT0tjJw,18855
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=gFOMB3ioTa7dxwoA9pYwpP2cDu2GvhBsJeMKT7XyzAw,185987
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=PYuSUclq9IZrKrS4iHP2EJ_-alRcLgXDXabmmenhda0,57970
92
+ ocpp/views.py,sha256=TRsfkm8VpygNPW7WIK_hQGX5a2ohrKnTcPfTM11WXVc,60141
93
93
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
- pages/admin.py,sha256=25vuZVdOJKMnTAnI1BUIyDhO8R-BuaAu8Wj56BNxyJ0,30349
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=oINGTI0owXz60FV-XFEjnTkY2xlSDE-W6X1TK8IK800,5072
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=6PMLiyuHAHbfLeHwwQxIVy2fJ32ramEO9SHAN05Set4,6967
101
- pages/models.py,sha256=Sp8e2VB5a7yg4eSUlz_VcsSlAuDVap26xBKYYggxmLM,20952
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=Lg3Jq_hOyF9KjiTeXr_AabFybW9KR3rvP5g_caomhGs,132912
106
- pages/urls.py,sha256=Ne6yYJxgUAMieDpppJ149E-yh-oVi92fARiRPe-n4-s,1166
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=Ye7qGlO7IwkZO0oR1SzCpkEDTtGCJPmDJT-x6QQ8vaQ,45848
109
- arthexis-0.1.18.dist-info/METADATA,sha256=C6OI88vHzosjqQvb7waV2pcPy0O5RbkOpdDBM-yOPBI,9998
110
- arthexis-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
111
- arthexis-0.1.18.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
112
- arthexis-0.1.18.dist-info/RECORD,,
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
- if not pending_items:
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 pending_items:
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 pending_items
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 pending_items:
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", "confirmed_peers")
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
- hostname = socket.gethostname()
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
- slug = slugify(hostname)
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
- response = requests.post(
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
- status_entry["status"] = "error"
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")