arthexis 0.1.18__py3-none-any.whl → 0.1.20__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.18
3
+ Version: 0.1.20
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
@@ -171,7 +171,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
171
171
  <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
172
172
  </tr>
173
173
  <tr>
174
- <td valign="top"><strong>Constellation</strong></td>
174
+ <td valign="top"><strong>Watchtower</strong></td>
175
175
  <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
176
176
  </tr>
177
177
  </tbody>
@@ -184,7 +184,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
184
184
  - **[Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)**: open [PowerShell](https://learn.microsoft.com/powershell/) or [Git Bash](https://gitforwindows.org/) and run the same command.
185
185
 
186
186
  ### 2. Start and stop
187
- Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Constellation roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
187
+ Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Watchtower roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
188
188
 
189
189
  - **[VS Code](https://code.visualstudio.com/)**
190
190
  - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
@@ -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.20.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,20 +10,20 @@ 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
17
17
  core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- core/admin.py,sha256=tVoWAdi4LzNaGQBOkvyIAmmALn3cXj-fU9rdYUPBYNg,145572
18
+ core/admin.py,sha256=3MhT1TA8QUAgmjlwBmLD6Z1nT19ZUrXLCBSchV1R6LE,151562
19
19
  core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
20
20
  core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
21
21
  core/apps.py,sha256=L_UMYI72-5jTo6nt8mfCbgdLhlP32D-8k76EZw0QyAA,14348
22
22
  core/auto_upgrade.py,sha256=1EffHHFylgydWdZM_id6CppV0QqBtdNw7cwBYVdbNdk,1715
23
- core/backends.py,sha256=Mzv_0YYF3iNWYAaKAJHHk75X2im-Kihu1zsg8eBeW2c,10509
23
+ core/backends.py,sha256=okrNW5Z-zDhJB9Al529aTOHGsTLACeQ6v_hMl4cyfnA,10704
24
24
  core/changelog.py,sha256=SRn37i5N-qb-RYV4Gpu9fg7Kv8gu4TH8ZwEmDRgN-Vo,12594
25
25
  core/entity.py,sha256=o4VteOXePGEsIWJFZ3fpq3DZsdWr3hpQ9A6kFbKosSE,4844
26
- core/environment.py,sha256=JLcvxAwU3OTL8O6kzwcUCFNZ3T28KanHrU_4mDBFamU,1584
26
+ core/environment.py,sha256=DfmVn2HVwX0rru-BSak6n3R3ier0UiHCZUoNszjt6oA,10182
27
27
  core/fields.py,sha256=d-qGahdcv4SRcO4fwCJ6_-NnEAP5xW0k3kODdAAAHSA,5412
28
28
  core/form_fields.py,sha256=h2xT8sO8EWbznsiARkxukFk69yoW6mQwqpgonA-d6aA,2496
29
29
  core/github_helper.py,sha256=fkjoUPwOB19zbGuk39LNLJ5AbIVKFf3rNCtnu-JISIc,5733
@@ -34,79 +34,79 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
34
34
  core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
35
35
  core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
36
36
  core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
37
- core/models.py,sha256=QIZazU5lwTaNQyO68Fm0Jg2AXDAjvg_HsK_djnKIv_Q,127829
38
- core/notifications.py,sha256=LYktoKM5k4q7YYWAJuqdeKM-p0Q-3gXgfqdq71qLS68,3916
37
+ core/models.py,sha256=U4oyaIuh1VSvpK7xwyZek09hRzVtY75zYXh1at7-_Pk,131276
38
+ core/notifications.py,sha256=jNLSuSCrhb8x5cDu_APeDlkrmbMejufk5eJOhssAC4I,3917
39
39
  core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
40
- core/reference_utils.py,sha256=jeox3V4cZNxzM2Jj31g_mdb3O55zy9S2iXAZu70R1Zc,3627
40
+ core/reference_utils.py,sha256=tffCoyE1w4_SmYzXVWOsW8aR_ZVVTSPzrGhBq8K2xzA,3631
41
41
  core/release.py,sha256=y5NRs0XwB7RQVvMEZoNWYjTBxuG68dOMizUXLRx7-x8,31561
42
42
  core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,3516
43
- core/sigil_builder.py,sha256=VLwbrrD7Zr3SHfIDYV-V7uv7LEGiIelCSkeGswHibuc,4843
43
+ core/sigil_builder.py,sha256=nMuhYlw3j3LosrK85Q0pYsMcfGWCmrmdnv8UG7GTq_o,4856
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=d0MQP5fmn5pA2VCFGxDMEX0xppDIIh8IAzPfGdnk8J4,12340
48
48
  core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
49
49
  core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
50
- core/tests.py,sha256=H0vPRcwcD3HNscRInbMGvdzs9ixvmkLh3d4YSEeQfEc,98679
50
+ core/tests.py,sha256=a78FZug8JPbvIMx1MgxmcYGyTpMr3WboTvqjnxI9evs,98715
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=U3hwNqJz_I9ybmmdJvLrljrynJaUzGMeHhsiaBJD4mk,90297
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=20gUgL5clMr_hNugybDqYdhoZh-4cWB2dVp6lgnHo_I,66960
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=S2Gc68Q2_8r8RO1ZKxASN5mEOj1q4UWtxdum5A-p0-4,71693
66
66
  nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
67
- nodes/rfid_sync.py,sha256=SP_BRUhgYMBH-zXrcM7uShgFSGYmmuIMb1OdcU1e-5U,6956
67
+ nodes/rfid_sync.py,sha256=oeblawcp6xeLApdIuhsJS83OAk58Eu7pVVmgpAc0Nt8,6953
68
68
  nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
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
69
+ nodes/tasks.py,sha256=7m9pKO-iI6JDdfPQ-GWRGown4mdyKrcroOnhbiWN7dY,5246
70
+ nodes/tests.py,sha256=OS9yI940avVZNNE9tn0i-Tr-BnyHKiUFKz0O1adNsUQ,176567
71
+ nodes/urls.py,sha256=-o9_pLo6XHerKMQwL0TW80wm6wmtVZqyNWcUhpdq9vk,915
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=ZaUlTLQUaRbQsiLCfTuwigxXSpoeUqBkrs938KsjmjA,36962
74
74
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- ocpp/admin.py,sha256=gMxHkT5KSp4kPWJcDJ1Y65VqgrwFTZl8Y516FO8oi3g,34658
75
+ ocpp/admin.py,sha256=1X2g-ICBJyp2Queh8Bexu8vXex6qU9onWN5yruczZ0s,38318
76
76
  ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
77
- ocpp/consumers.py,sha256=s51784IwUoWfLaMI_zfnCve7ouZIB5xAnlgwThb40Gw,67979
77
+ ocpp/consumers.py,sha256=wt9tauYSlpWnGbaGsJKIPt4D13HyloLIHwmlK8iqA0Q,71674
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=wUGYSh382e_zYXq059GCaWqUCjQTTvYbbt0PdlONMFc,34411
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
86
- ocpp/tasks.py,sha256=OxIaI4OSLz9AfwLexnXhiBILBimTs3gVrPd197Jguqg,5819
87
- ocpp/test_export_import.py,sha256=Zp6xUBlRq7XkdKjOs78BhkujNQdklxi4RLxU8c-udWY,4530
88
- ocpp/test_rfid.py,sha256=hMFQwYDPhwfTW2XdDT5q__gKrL8YPbv7DNNMdwzJ7BQ,39105
89
- ocpp/tests.py,sha256=gFOMB3ioTa7dxwoA9pYwpP2cDu2GvhBsJeMKT7XyzAw,185987
90
- ocpp/transactions_io.py,sha256=YnxI-Tv5UFxv0JuFK3XpoqFYP8eRT8sMuDiqkiMHPtU,7387
91
- ocpp/urls.py,sha256=3T5O5DSwVk4PbhPx5p4D3UseCWvC5xV5HwJLSM6AfA8,1700
92
- ocpp/views.py,sha256=PYuSUclq9IZrKrS4iHP2EJ_-alRcLgXDXabmmenhda0,57970
85
+ ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
86
+ ocpp/tasks.py,sha256=n2axf1Oo7brZtXRe-uCwt6K0f57ZlHUNvp6c5W0Gdzo,6035
87
+ ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
88
+ ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
89
+ ocpp/tests.py,sha256=8Tzb_agTQdLdHnccAGCTfYbcRQHznz5DJI3Hb94mg5M,190065
90
+ ocpp/transactions_io.py,sha256=p2aUsKlCDYnZ4ZBrOM7pxXoW_w3Tbm-tvRFSjnR3x24,7738
91
+ ocpp/urls.py,sha256=5ZomUtznJe3kfs8E-DtVp12eFva5jUuJdpTEczIsQ5w,1730
92
+ ocpp/views.py,sha256=G2y2g55XJBJ0gzZPUqA4pplKfQIrO3-fjH5CkOYqDsw,63152
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
98
- pages/defaults.py,sha256=l36APPAZO4ub2A8Pp-lQGujKeOVYcyzU6t7-kOk8VoA,522
97
+ pages/context_processors.py,sha256=0Ie3_lPY6-j6f8eXtSzMlWpprMzY0tvX9fcOdpJr6Vs,5788
98
+ pages/defaults.py,sha256=3tjv3nFPxwpFu6poJ1Ez1MP92Q6ZvyRluftKHlU-zeI,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
102
- pages/module_defaults.py,sha256=R8n6eQDjNRMpO-DW86OFGvyRarju5Bx7Fnb275R_z24,5411
100
+ pages/middleware.py,sha256=MYd5Nko4AnFg3orY6MuyvvNg_I6GCIf8mDW8znSOgvQ,7042
101
+ pages/models.py,sha256=Ms_m_tzzstNghN_JOzyfwsbllDBIl_AKnYdTRTJthqM,22173
102
+ pages/module_defaults.py,sha256=rCAY8aTyxYNL0M5zDr393rX-Gi-svXqKtuLXm0rILrQ,5444
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=rgMYFiei8ONBNts4vJzukjaWN-WUGLxr6LFwtSHJQ_8,141550
106
+ pages/urls.py,sha256=rcK0zEJTSCurYhx77ZK_GVVMSHTS5ZJWoKTJkh1lj-M,1305
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=2ik28HW70a0S84K8hMQ8qL9yzVH1WggXT7S8G5ZaLgA,50940
109
+ arthexis-0.1.20.dist-info/METADATA,sha256=RLz9veHKDPM6UzcxnJgZcxj3ri6TuTPVLSK9QBn48mM,11751
110
+ arthexis-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
111
+ arthexis-0.1.20.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
112
+ arthexis-0.1.20.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/admin.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from io import BytesIO
2
3
  import os
3
4
 
@@ -2909,7 +2910,7 @@ class CopyRFIDForm(forms.Form):
2909
2910
  normalized = (cleaned or "").strip().upper()
2910
2911
  if not normalized:
2911
2912
  raise forms.ValidationError(_("RFID value is required."))
2912
- if RFID.objects.filter(rfid=normalized).exists():
2913
+ if RFID.matching_queryset(normalized).exists():
2913
2914
  raise forms.ValidationError(
2914
2915
  _("An RFID with this value already exists.")
2915
2916
  )
@@ -2943,6 +2944,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2943
2944
  "print_card_labels",
2944
2945
  "print_release_form",
2945
2946
  "copy_rfids",
2947
+ "merge_rfids",
2946
2948
  "toggle_selected_user_data",
2947
2949
  "toggle_selected_released",
2948
2950
  "toggle_selected_allowed",
@@ -3177,6 +3179,145 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3177
3179
  context["media"] = self.media + form.media
3178
3180
  return TemplateResponse(request, "admin/core/rfid/copy.html", context)
3179
3181
 
3182
+ @admin.action(description=_("Merge RFID cards"))
3183
+ def merge_rfids(self, request, queryset):
3184
+ tags = list(queryset.prefetch_related("energy_accounts"))
3185
+ if len(tags) < 2:
3186
+ self.message_user(
3187
+ request,
3188
+ _("Select at least two RFIDs to merge."),
3189
+ level=messages.WARNING,
3190
+ )
3191
+ return None
3192
+
3193
+ normalized_map: dict[int, str] = {}
3194
+ groups: defaultdict[str, list[RFID]] = defaultdict(list)
3195
+ unmatched = 0
3196
+ for tag in tags:
3197
+ normalized = RFID.normalize_code(tag.rfid)
3198
+ normalized_map[tag.pk] = normalized
3199
+ if not normalized:
3200
+ unmatched += 1
3201
+ continue
3202
+ prefix = normalized[: RFID.MATCH_PREFIX_LENGTH]
3203
+ groups[prefix].append(tag)
3204
+
3205
+ merge_groups: list[list[RFID]] = []
3206
+ skipped = unmatched
3207
+ for prefix, group in groups.items():
3208
+ if len(group) < 2:
3209
+ skipped += len(group)
3210
+ continue
3211
+ group.sort(
3212
+ key=lambda item: (
3213
+ len(normalized_map.get(item.pk, "")),
3214
+ normalized_map.get(item.pk, ""),
3215
+ item.pk,
3216
+ )
3217
+ )
3218
+ merge_groups.append(group)
3219
+
3220
+ if not merge_groups:
3221
+ self.message_user(
3222
+ request,
3223
+ _("No matching RFIDs were found to merge."),
3224
+ level=messages.WARNING,
3225
+ )
3226
+ return None
3227
+
3228
+ merged_tags = 0
3229
+ merged_groups = 0
3230
+ conflicting_accounts = 0
3231
+ with transaction.atomic():
3232
+ for group in merge_groups:
3233
+ canonical = group[0]
3234
+ update_fields: set[str] = set()
3235
+ existing_account_ids = set(
3236
+ canonical.energy_accounts.values_list("pk", flat=True)
3237
+ )
3238
+ for tag in group[1:]:
3239
+ other_value = normalized_map.get(tag.pk, "")
3240
+ if canonical.adopt_rfid(other_value):
3241
+ update_fields.add("rfid")
3242
+ normalized_map[canonical.pk] = RFID.normalize_code(
3243
+ canonical.rfid
3244
+ )
3245
+ accounts = list(tag.energy_accounts.all())
3246
+ if accounts:
3247
+ transferable: list[EnergyAccount] = []
3248
+ for account in accounts:
3249
+ if existing_account_ids and account.pk not in existing_account_ids:
3250
+ conflicting_accounts += 1
3251
+ continue
3252
+ transferable.append(account)
3253
+ if transferable:
3254
+ canonical.energy_accounts.add(*transferable)
3255
+ existing_account_ids.update(
3256
+ account.pk for account in transferable
3257
+ )
3258
+ if tag.allowed and not canonical.allowed:
3259
+ canonical.allowed = True
3260
+ update_fields.add("allowed")
3261
+ if tag.released and not canonical.released:
3262
+ canonical.released = True
3263
+ update_fields.add("released")
3264
+ if tag.key_a_verified and not canonical.key_a_verified:
3265
+ canonical.key_a_verified = True
3266
+ update_fields.add("key_a_verified")
3267
+ if tag.key_b_verified and not canonical.key_b_verified:
3268
+ canonical.key_b_verified = True
3269
+ update_fields.add("key_b_verified")
3270
+ if tag.last_seen_on and (
3271
+ not canonical.last_seen_on
3272
+ or tag.last_seen_on > canonical.last_seen_on
3273
+ ):
3274
+ canonical.last_seen_on = tag.last_seen_on
3275
+ update_fields.add("last_seen_on")
3276
+ if not canonical.origin_node and tag.origin_node_id:
3277
+ canonical.origin_node = tag.origin_node
3278
+ update_fields.add("origin_node")
3279
+ merged_tags += 1
3280
+ tag.delete()
3281
+ if update_fields:
3282
+ canonical.save(update_fields=sorted(update_fields))
3283
+ merged_groups += 1
3284
+
3285
+ if merged_tags:
3286
+ self.message_user(
3287
+ request,
3288
+ ngettext(
3289
+ "Merged %(removed)d RFID into %(groups)d canonical record.",
3290
+ "Merged %(removed)d RFIDs into %(groups)d canonical records.",
3291
+ merged_tags,
3292
+ )
3293
+ % {"removed": merged_tags, "groups": merged_groups},
3294
+ level=messages.SUCCESS,
3295
+ )
3296
+
3297
+ if skipped:
3298
+ self.message_user(
3299
+ request,
3300
+ ngettext(
3301
+ "Skipped %(count)d RFID because it did not share the first %(length)d characters with another selection.",
3302
+ "Skipped %(count)d RFIDs because they did not share the first %(length)d characters with another selection.",
3303
+ skipped,
3304
+ )
3305
+ % {"count": skipped, "length": RFID.MATCH_PREFIX_LENGTH},
3306
+ level=messages.WARNING,
3307
+ )
3308
+
3309
+ if conflicting_accounts:
3310
+ self.message_user(
3311
+ request,
3312
+ ngettext(
3313
+ "Skipped %(count)d energy account because the RFID was already linked to a different account.",
3314
+ "Skipped %(count)d energy accounts because the RFID was already linked to a different account.",
3315
+ conflicting_accounts,
3316
+ )
3317
+ % {"count": conflicting_accounts},
3318
+ level=messages.WARNING,
3319
+ )
3320
+
3180
3321
  def _render_card_labels(
3181
3322
  self,
3182
3323
  request,
core/backends.py CHANGED
@@ -81,10 +81,16 @@ class RFIDBackend:
81
81
  if not rfid_value:
82
82
  return None
83
83
 
84
- tag = RFID.objects.filter(rfid=rfid_value).first()
85
- if not tag or not tag.allowed:
84
+ tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
85
+ if not tag:
86
86
  return None
87
87
 
88
+ update_fields: list[str] = []
89
+ if tag.adopt_rfid(rfid_value):
90
+ update_fields.append("rfid")
91
+ if update_fields:
92
+ tag.save(update_fields=update_fields)
93
+
88
94
  command = (tag.external_command or "").strip()
89
95
  if command:
90
96
  env = os.environ.copy()