arthexis 0.1.9__py3-none-any.whl → 0.1.10__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.9
3
+ Version: 0.1.10
4
4
  Summary: Django-based MESH system
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -43,6 +43,7 @@ Requires-Dist: django-celery-beat==2.8.1
43
43
  Requires-Dist: django-debug-toolbar==6.0.0
44
44
  Requires-Dist: django-import-export==4.3.9
45
45
  Requires-Dist: django-object-actions==5.0.0
46
+ Requires-Dist: django-otp==1.5.4
46
47
  Requires-Dist: django-timezone-field==7.1
47
48
  Requires-Dist: dnspython==2.7.0
48
49
  Requires-Dist: docutils==0.22
@@ -63,6 +64,7 @@ Requires-Dist: outcome==1.3.0.post0
63
64
  Requires-Dist: packaging==25.0
64
65
  Requires-Dist: pillow==11.3.0
65
66
  Requires-Dist: prompt_toolkit==3.0.51
67
+ Requires-Dist: psutil==5.9.8
66
68
  Requires-Dist: psycopg==3.2.9
67
69
  Requires-Dist: psycopg-binary==3.2.9
68
70
  Requires-Dist: pyasn1==0.6.1
@@ -112,50 +114,91 @@ Dynamic: license-file
112
114
 
113
115
  # Arthexis Constellation
114
116
 
117
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml)
118
+
115
119
  ## Purpose
116
120
 
117
121
  Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Narrative) [Django](https://www.djangoproject.com/)-based [software suite](https://en.wikipedia.org/wiki/Software_suite) that centralizes tools for managing [electric vehicle charging infrastructure](https://en.wikipedia.org/wiki/Charging_station) and orchestrating [energy](https://en.wikipedia.org/wiki/Energy)-related [products](https://en.wikipedia.org/wiki/Product_(business)) and [services](https://en.wikipedia.org/wiki/Service_(economics)).
118
122
 
119
123
  ## Features
120
124
 
121
- - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/)
122
- - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/) 1.6
125
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system, handling:
126
+ - Lifecycle & sessions: BootNotification, Heartbeat, StatusNotification, StartTransaction, StopTransaction
127
+ - Access & metering: Authorize, MeterValues
128
+ - Maintenance & firmware: DiagnosticsStatusNotification, FirmwareStatusNotification
129
+ - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
130
+ - Employee credentials via `res.users`
131
+ - Product catalog lookups via `product.product`
123
132
  - Runs on [Windows 11](https://www.microsoft.com/windows/windows-11) and [Ubuntu 22.04 LTS](https://releases.ubuntu.com/22.04/)
124
133
  - Tested for the [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
125
134
 
126
135
  Project under active development.
127
136
 
128
- ## Four Role Architecture
137
+ ## Role Architecture
129
138
 
130
139
  Arthexis Constellation ships in four node roles tailored to different deployment scenarios.
131
140
 
132
- | Role | Description & Common Features |
133
- | --- | --- |
134
- | Terminal | Single-User Research & Development<br>Features: GUI Toast |
135
- | Control | Single-Device Testing & Special Task Appliances<br>Features: AP Public Wi-Fi, Celery Queue, GUI Toast, LCD Screen, NGINX Server, RFID Scanner |
136
- | Satellite | Multi-Device Edge, Network & Data Acquisition<br>Features: AP Router, Celery Queue, NGINX Server, RFID Scanner |
137
- | Constellation | Multi-User Cloud & Orchestration<br>Features: Celery Queue, NGINX Server |
141
+ <table border="1" cellpadding="8" cellspacing="0">
142
+ <thead>
143
+ <tr>
144
+ <th align="left">Role</th>
145
+ <th align="left">Description &amp; Common Features</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ <tr>
150
+ <td valign="top"><strong>Terminal</strong></td>
151
+ <td valign="top"><strong>Single-User Research &amp; Development</strong><br />Features: GUI Toast</td>
152
+ </tr>
153
+ <tr>
154
+ <td valign="top"><strong>Control</strong></td>
155
+ <td valign="top"><strong>Single-Device Testing &amp; Special Task Appliances</strong><br />Features: AP Public Wi-Fi, Celery Queue, GUI Toast, LCD Screen, NGINX Server, RFID Scanner</td>
156
+ </tr>
157
+ <tr>
158
+ <td valign="top"><strong>Satellite</strong></td>
159
+ <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
160
+ </tr>
161
+ <tr>
162
+ <td valign="top"><strong>Constellation</strong></td>
163
+ <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
164
+ </tr>
165
+ </tbody>
166
+ </table>
138
167
 
139
168
  ## Quick Guide
140
169
 
141
170
  ### 1. Clone
142
- - **[Linux](https://en.wikipedia.org/wiki/Linux)**: open a [terminal](https://en.wikipedia.org/wiki/Command-line_interface) and run
143
- `git clone https://github.com/arthexis/arthexis.git`
171
+ - **[Linux](https://en.wikipedia.org/wiki/Linux)**: open a [terminal](https://en.wikipedia.org/wiki/Command-line_interface) and run `git clone https://github.com/arthexis/arthexis.git`.
144
172
  - **[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.
145
173
 
146
174
  ### 2. Start and stop
147
- - **[VS Code](https://code.visualstudio.com/)**: open the folder, go to the
148
- **Run and Debug** panel (`Ctrl+Shift+D`), select the **Run Server** (or
149
- **Debug Server**) configuration, and press the green start button. Stop the
150
- server with the red square button (`Shift+F5`).
151
- - **[Shell](https://en.wikipedia.org/wiki/Shell_(computing))**: on Linux run [`./start.sh`](start.sh) and stop with [`./stop.sh`](stop.sh); on Windows run [`start.bat`](start.bat) and stop with `Ctrl+C`.
175
+ 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.
176
+
177
+ **[VS Code](https://code.visualstudio.com/)**
178
+ - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
179
+ - Select the **Run Server** (or **Debug Server**) configuration.
180
+ - Press the green start button. Stop the server with the red square button (`Shift+F5`).
181
+
182
+ **[Shell](https://en.wikipedia.org/wiki/Shell_(computing))**
183
+ - Linux: run [`./start.sh`](start.sh) and stop with [`./stop.sh`](stop.sh).
184
+ - Windows: run [`start.bat`](start.bat) and stop with `Ctrl+C`.
152
185
 
153
186
  ### 3. Install and upgrade
154
- - **Linux**: use [`./install.sh`](install.sh) with options like `--service NAME`, `--public` or `--internal`, `--port PORT`, `--upgrade`, `--auto-upgrade`, `--latest`, `--celery`, `--lcd-screen`, `--no-lcd-screen`, `--clean`, `--datasette`. Upgrade with [`./upgrade.sh`](upgrade.sh) using flags such as `--latest`, `--clean`, or `--no-restart`.
155
- - **Windows**: run [`install.bat`](install.bat) to install and [`upgrade.bat`](upgrade.bat) to upgrade.
187
+ **Linux:** run [`./install.sh`](install.sh) with a node role flag:
188
+ - `--terminal` default when unspecified and recommended if you're unsure. Terminal nodes can also use the start/stop scripts above without installing.
189
+ - `--control` – prepares the single-device testing appliance.
190
+ - `--satellite` – configures the edge data acquisition node.
191
+ - `--constellation` – enables the multi-user orchestration stack.
192
+ Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
193
+
194
+ Upgrade with [`./upgrade.sh`](upgrade.sh).
195
+
196
+ **Windows:**
197
+ - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
198
+ - Installation is not required to start in Terminal mode (the default).
156
199
 
157
200
  ### 4. Administration
158
- Visit [`http://localhost:8888/admin/`](http://localhost:8888/admin/) for the [Django admin](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) and [`http://localhost:8888/admindocs/`](http://localhost:8888/admindocs/) for the [admindocs](https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/). Use port `8000` if you started with [`start.bat`](start.bat) or the `--public` option.
201
+ 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.
159
202
 
160
203
  ## Support
161
204
 
@@ -1,4 +1,4 @@
1
- arthexis-0.1.9.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
1
+ arthexis-0.1.10.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
2
2
  config/__init__.py,sha256=8_b7rx_-Xcuzu3Z7mSR94q3PAhjyYqLFQi3IOEz6hcI,108
3
3
  config/active_app.py,sha256=MET_G7oHL7GkoSo3VkkMzymM-PwsSZazMLZxpgjFLTo,388
4
4
  config/asgi.py,sha256=n09URedOmQ_59II3UCl3iodGSDWOuN_A8DFyfLjuylA,803
@@ -10,18 +10,19 @@ config/loadenv.py,sha256=bhFbHTbRJSkSwrFk3UInKEKQ8ZY-poatOGi7rC57YAI,298
10
10
  config/logging.py,sha256=334jADN4dM5GNHaCWlYPOKYa5BhfxbsuejH_QDALG6g,1793
11
11
  config/middleware.py,sha256=EvraDumepnKwCDswHGXb1mK7vud_dEEoZ4eh0IQ7fhQ,744
12
12
  config/offline.py,sha256=mhQjCUzdOwSzZ6oLgPDJR48xaPIDzOi34ARUEz43seE,1431
13
- config/settings.py,sha256=wRfx3EH5YHuiSB73Xnq-RqLtkLzISBEPwpcXRhqh-ws,14696
14
- config/urls.py,sha256=Eap8bmBfKAA43H05QpAJQfLrGamJYHwmOx4j663EJpU,4950
13
+ config/settings.py,sha256=TQI_hBt0XhDnggDcMlLu8R_5xsSP04keONR2e8Udo30,21764
14
+ config/urls.py,sha256=-ehzyYSbOTWC_QOtgxNCPMnjJZJNRoGn8Ww7-t2D9Do,5143
15
15
  config/wsgi.py,sha256=Fu-ONO2SgYeU6rhmy909P-uLX-n8ALJQObdm9MHPS-k,450
16
16
  core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- core/admin.py,sha256=5Dv16tphT5JoCjn1oLiLMfKKJOIeNjz1AZmmgMc6eBs,71319
17
+ core/admin.py,sha256=f61hqWOI1RCydHJQ9XGO9ULFLd7YcPSfsnZvovcCGgk,87335
18
18
  core/admin_history.py,sha256=NIDWkosJoHMaucBvUjq8VmmL-0e8ngJ4l4XA89d4jwQ,1833
19
19
  core/admindocs.py,sha256=GufdugiNEG87xGSDYVq4CBMhGRubsQCzgz-FqDIqzpM,5367
20
- core/apps.py,sha256=YFto4vdVW4uq98s-HypSAZx-q3HXDbKuyU7rYaqVDYs,9059
21
- core/backends.py,sha256=fOf64Ajg1arDUzz9yDCvFw5T4ZoRljurIUlcT9GYBHM,4540
20
+ core/apps.py,sha256=gg8wxmrG7455a2i6xITO-akOc5daG87HJ3hhEseFcgo,9263
21
+ core/auto_upgrade.py,sha256=BkoE7rJuYAmwoMux22NqujWZYjYXtN40GBloC0sNMY4,1799
22
+ core/backends.py,sha256=y7ywo9EliuHItP-cOe_lIn0R7PpgcSq92PZVn_XSW3w,6936
22
23
  core/entity.py,sha256=dkPywTVk981fV8bOEoZw-1SMrdh8T0jVAUZnRxg3dDg,4505
23
24
  core/environment.py,sha256=QcOshpWNG0l_agW-b9efNvVFKqdatj6sUK8FT6p92gU,1238
24
- core/fields.py,sha256=fe9XupyF7K-5L6Dpxt6by_sQXoxSxf3F_eZpFSf_uFk,2459
25
+ core/fields.py,sha256=4nJ_tngso8NHs6HBl8nn5PWvbcOiuAgkoM6hixrFm64,5580
25
26
  core/github_helper.py,sha256=k9LmxL0Ec5MzFwZsbMpWMMJ-5tGMe-5yRoI8V0sbFsw,682
26
27
  core/github_issues.py,sha256=LKqt-Ilx5TRYnx2LXttc54Nvuh3_1VRP0u_M3whe09c,4963
27
28
  core/lcd_screen.py,sha256=mkKtIJjHGDLaV4t80L-JScsZXbLhlYUDknAcvV9Ijr0,2651
@@ -29,64 +30,66 @@ core/liveupdate.py,sha256=kTgbE2gnU3PPIV-88Bw2swSl1aGp6stSdBYqBFbLvx0,716
29
30
  core/log_paths.py,sha256=6UXYk6QIUmRO3ecFaFH3dgJ_pf4C_wUN6b0JqUhLVBY,3045
30
31
  core/mailer.py,sha256=ziw6r4AhMcMRMtNOrcqRXimjo17SooaJJ-fkSVfo_aI,2787
31
32
  core/middleware.py,sha256=a4XL0pld4YiG-vanHrzYbJNHv64s75lvmG9inoG1ln0,3479
32
- core/models.py,sha256=rkyUZkonxByZi-QnKxPskSD23voC-x-Ud62vFbBSW9U,75354
33
+ core/models.py,sha256=iUTgJIBDC1SHqaKtu3AF48YgMg-eCsbF0U79UmNyOSM,82274
33
34
  core/notifications.py,sha256=YtNDGxNveZ6t3tlMXJ7wIaZZTWIZfOKy6vN9mXkeYnA,4021
34
35
  core/public_wifi.py,sha256=A08IPJqcdgUKSWbktyqsV4ol8C0uxDxZy1s1ECuPdBE,6526
36
+ core/reference_utils.py,sha256=vsjF4XaZAAluVbEu6xqQDR1xtHPConXwu2px-4KlllY,3224
35
37
  core/release.py,sha256=thtgbfAnxHlOODM8xUVETgY5aAIbddWan0wW9lKmtQI,11064
36
- core/sigil_builder.py,sha256=XxzL0d-GDtHR93_gKNChEWWHb40t8V-CJRQGCzaHEHg,4458
38
+ core/sigil_builder.py,sha256=NdTqNT8Bdo32Bf2h3ciy48C6zZIIoWTrm8TjzOtlThc,4854
37
39
  core/sigil_context.py,sha256=8xrGiB2L1dFfSTrVLsFPLKfkhRwCXXZ0-EXvZdPeTMU,459
38
40
  core/sigil_resolver.py,sha256=kRzkv669R1JggQ7eAXsIfw8kH9wuB8L71pmZYg4vwqs,10108
39
- core/system.py,sha256=wzhWpV1uC1YGHZI_2eJvr3yyj5FgQEIoD1cWCAfDkHo,7866
40
- core/tasks.py,sha256=WBA2WLHNIDhioV7ibTJZLTx_Ix3738qYlP6BpHooJug,5772
41
- core/test_system_info.py,sha256=2uIKVf38Z-isGZJAVmqu8uAYQ8vTBMuLoWpeMQ7Xi0s,1294
42
- core/tests.py,sha256=Uzzg6nWMJiMtN4Yi3fDAbza82rHMT8oXPaAapKPZMpg,42559
41
+ core/system.py,sha256=26lDZOauEUvsQEq24N_BMMvAINlR85urSHEjX5zm1rI,7461
42
+ core/tasks.py,sha256=hwowcVsbwME-75KN90D2r0kX_YPMZEVevOrrxIxpM34,10595
43
+ core/test_system_info.py,sha256=d2_SgIUWwqWibOpesliSC7mzK5IyEN6P-5DZWWGmTwI,2605
44
+ core/tests.py,sha256=4dzMoHbhbKLRTfQJMa57s-ldd7mt0kSCfpI2Sj-X7lU,53954
43
45
  core/tests_liveupdate.py,sha256=D1o2gPopnK7wDCeDQlJ-tfitWh4umZQFRxSTCFY-puc,527
44
46
  core/urls.py,sha256=x-LNCxCgrLINdBsJTUUcAuMS5EK5Sh1ybvsVuUnJfLw,436
45
- core/user_data.py,sha256=p5Y7m9gqV8SHaiPtkIfEsdk7oopqh-pA2ohRPJXnzH8,17186
46
- core/views.py,sha256=RAk9ZEP6Zl2isijsC1AL_jwhMDHjTH-jTJjHNsw-7fY,27929
47
+ core/user_data.py,sha256=69AFm-HKM2SqNs77O-k5vRkWB7u4zrdlUQoUsB1-oPM,20142
48
+ core/views.py,sha256=9Fzkvt2O48H4AljFAapdr8Lst9KsAYLmEarcYMQ0bcA,35873
47
49
  core/widgets.py,sha256=ihah_-NtFJ3oRCS3TdcT6iHCUTlg1tUULJsVCu05C0o,1379
48
50
  core/workgroup_urls.py,sha256=2bC8mOMkxIj04WsNis0Td6AmmJFr6z27Ol5epIvhO28,424
49
51
  core/workgroup_views.py,sha256=pFJ4PIRN3WWpRyombpWDKKteYQYoI7lu7ddSEKojD7I,2983
50
52
  nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
53
  nodes/actions.py,sha256=HHnwByTBc3guOORvrKOuvUFID-_BpBq6OENE_PDgk9s,2335
52
- nodes/admin.py,sha256=6wmUDTpCFyZdUt8Ef7c8c347JJ-BOPRPlADFMsUxd9Y,16560
54
+ nodes/admin.py,sha256=o0iiNzHE7fNv8bpj7VAdTGboT05hVCCtrTdknAzevZE,16736
53
55
  nodes/apps.py,sha256=KijGydsQBS-8Q8uBc0ahDZXSSCMzP3tQcYIEigJxdCE,2077
54
- nodes/backends.py,sha256=cHMpliDO453O8gVmtBRTg2VT0tZ1XL_cgneMyuo3In8,2080
56
+ nodes/backends.py,sha256=dQMbC-T9vSxvXMxssi0Y2ZKmc8LbSM_h4lm3EDbtZeo,5525
55
57
  nodes/lcd.py,sha256=7MqS3jV_De2ysSmTtXQfYTWaxdfbmm6YfNdb_7qIVZc,5923
56
- nodes/models.py,sha256=Hq-4dK0IxSZ0JSAGjX4wlTBT0AZFDE6Y6801Rl1pFv4,30229
58
+ nodes/models.py,sha256=mvryiS3S3ZT0sOpANGJYzA1P3W00kT-InIgXzwFFsao,37629
57
59
  nodes/tasks.py,sha256=jorbN4h0PqWMBRMFdkGgJtvG8GPFwGr5Hxlm5In3EeY,1568
58
- nodes/tests.py,sha256=CqZwGQRrqqVFcIunfnLaP0S3eV0alBBPhZl8LKsh308,59797
60
+ nodes/tests.py,sha256=1J97W3r12EOpJ8YY-3Ak3mWD0YGwo5zkGjA0heAzdV8,75302
59
61
  nodes/urls.py,sha256=20yZDZf4DNgIZ9hQWsUzjp8k5Fryg9ukk761_KGQt9k,548
60
62
  nodes/utils.py,sha256=B9BD3bKBkwDjIqyp0pyjnKQQlRbGRVQndfoaMEJoNDc,2815
61
- nodes/views.py,sha256=uUHDTgARj4eOUOrEUCx41GZzWajT5vHSa9svcAuLYVo,10261
63
+ nodes/views.py,sha256=qBEu19h64cTyn9YuuL9YRnqymCdZPsmMbjXedjd9fzY,14988
62
64
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- ocpp/admin.py,sha256=sHJaSfFFx6T1J4WBKp_-_Y0zJXZDCJJxFpXrOgH6jTg,14825
65
+ ocpp/admin.py,sha256=Rw0PinweVSaBjIQIcgj_cgfalNRBHcrAh3JInx8Hn7A,17129
64
66
  ocpp/apps.py,sha256=mCZ5Z0ei7z7c62luIcIhbEuwL1N4czQFSToOkGvRmms,867
65
- ocpp/consumers.py,sha256=C7ZfTuGxf3EDt2cizlSxsdn1KnbpbqUb-rfIUD-mKr4,28171
66
- ocpp/evcs.py,sha256=TppUZ9ACaiXsfhF_DsL0I9jEyZFPbdUpFWP9Ljbe6gI,33973
67
- ocpp/models.py,sha256=RXfdlZ268otOZsWQVEcc0n0YmsrDqserHFNSSuCFIzs,22253
67
+ ocpp/consumers.py,sha256=xxfQxSeFvP39Mq5N_pWDlWNCSdgMcAeV03oiOhr3SYk,36392
68
+ ocpp/evcs.py,sha256=O53rCHdxKcgPsj7o57rDiNHTVvEii3DTtQ3djWFTohw,34065
69
+ ocpp/models.py,sha256=bGzbU68o7-oIKOHvE1_pqj9B-KLDP1mnT-T-wlOsI_4,23801
68
70
  ocpp/routing.py,sha256=g9vPnLw-D1N8L_mW0_oCe-nTDibjC0Et-SFxe8NFAOI,308
69
- ocpp/simulator.py,sha256=iS9Y-ZRdQXd6TuRoviDOVtOgyDtc4in4FkKFwmTrZD8,13764
70
- ocpp/store.py,sha256=h4Mq82dR_8LIXelD99ND12HSHWHEvWhPNNXMyDAwm1M,13247
71
+ ocpp/simulator.py,sha256=es6SJNzKUwLwrLy-zDdRu__n34V2RZSQRb8SeZHh4Fw,15636
72
+ ocpp/store.py,sha256=FyyZW2YKTWleuNdHTo_RsUO2InZZJhvMYyuZmLgQZe4,14051
71
73
  ocpp/tasks.py,sha256=cOcJBshckFKs8GnACvmYZUBG116amtLRAzEP-JNqlZ0,905
72
74
  ocpp/test_export_import.py,sha256=TK1__E4K5WrLYPIx-1iJWoIhuRCnOtXc2cYEpGigd3U,4660
73
75
  ocpp/test_rfid.py,sha256=1Xw54WNLzIv4IvvKbQEAs3jtdcg5sTqQBLJK9ZzCqwE,19301
74
- ocpp/tests.py,sha256=4JCXBL5ieIl4rAiIUFB_aCN6vLNa4OkFF8DdylFysR0,90789
76
+ ocpp/tests.py,sha256=-wjEccNB2zBGiOd9WNXzh_8wlBdIcnfV60gmT8Eimng,106222
75
77
  ocpp/transactions_io.py,sha256=OvRynP3DeC9ZpHD3Ez-0YPNPJXFcdSOSKg0nRrwzBJ0,6507
76
78
  ocpp/urls.py,sha256=jl6AQLtmLMvrNXP3dQKgPe9Kvaul4oaYd80DskpzVBc,1750
77
- ocpp/views.py,sha256=lTxK23r62ridqoSNkj_us7IokQ9T0RYZMBtr24F2ef8,35879
79
+ ocpp/views.py,sha256=QlpP8gIXWHqm0pAhQKzlDUmRH2-cTJnJPELuRxV78dw,36781
78
80
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
81
  pages/admin.py,sha256=IdaAibuYJbRMuxw9vniTcuToGCaRFJewwXQ2XQ-1384,12476
80
82
  pages/apps.py,sha256=mfCxegmnRqKcszyEwpQhZpW9JWOuEYdVereU_w49BXg,298
81
83
  pages/checks.py,sha256=an-MlMCIG-FSKmdgOhYV0VOoB_wDQD7dxKD03Hjrzts,1567
82
- pages/context_processors.py,sha256=Ph4PEaq8NbiG50idlBXHzMgMGgyX5aVg1rBmb9bOvcs,2897
84
+ pages/context_processors.py,sha256=VkApl_Krku1k2CTDt9Xw7OlqF7SN8reLEo9kF7KKxiU,3592
85
+ pages/forms.py,sha256=NuG7ONP1HYY6O5gRoQaRSwSRchcSonRm3rulsqGSqvY,5081
83
86
  pages/middleware.py,sha256=fUdAscLa6h2EqJTFUjhVijfSf82gBfm6XUZTVygWAnk,5227
84
87
  pages/models.py,sha256=VBGpZVQrOHR6ShvqY5MT_Um8rVUktbKMPnq9KJCsTRo,8375
85
- pages/tests.py,sha256=gOxh0invZL2vcyXY9QhfPxO6OmUwoOWVNFQwvynoZf8,56715
86
- pages/urls.py,sha256=7V2nI74v1SdsQ6_vN-8VbOJA_htP3uoogq1ISYmAlpY,687
88
+ pages/tests.py,sha256=zVdoRVu4IE7go5d7Xd7eceyqQitstwCQ0S7OIma4qhc,73823
89
+ pages/urls.py,sha256=wFHov2KAW4vKPScyPpMY9d6rOrKyJHFbOMc9y7L4gEQ,777
87
90
  pages/utils.py,sha256=7kik1W0Gk6SFxYGhg6shfI2W9Xdcv1sCpkRCQ883a88,311
88
- pages/views.py,sha256=VXciSuB4798pCHG7ZaOExQSC3zL1OSP0KeuyUja_xUk,28854
89
- arthexis-0.1.9.dist-info/METADATA,sha256=WMykpJVayOXcPMDi4bi__VU1mj-kJNGAfKlL7sOW2I0,8029
90
- arthexis-0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
91
- arthexis-0.1.9.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
92
- arthexis-0.1.9.dist-info/RECORD,,
91
+ pages/views.py,sha256=vebvTOX3MCCugUsoArqJJ0VDIJJpAjPvJuPQPiLt9p0,38810
92
+ arthexis-0.1.10.dist-info/METADATA,sha256=UBWs7YFz5bZxreIVPf6ugBOGp3KDtvF7iwSJVtKpSo0,9800
93
+ arthexis-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
94
+ arthexis-0.1.10.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
95
+ arthexis-0.1.10.dist-info/RECORD,,
config/settings.py CHANGED
@@ -20,6 +20,7 @@ from core.log_paths import select_log_dir
20
20
  from django.utils.translation import gettext_lazy as _
21
21
  from celery.schedules import crontab
22
22
  from django.http import request as http_request
23
+ from django.http.request import split_domain_port
23
24
  from django.middleware.csrf import CsrfViewMiddleware
24
25
  from django.core.exceptions import DisallowedHost
25
26
  from django.contrib.sites import shortcuts as sites_shortcuts
@@ -34,13 +35,36 @@ if not hasattr(encoding, "force_text"): # pragma: no cover - Django>=5 compatib
34
35
  encoding.force_text = force_str
35
36
 
36
37
 
38
+
37
39
  _original_validate_host = http_request.validate_host
38
40
 
39
41
 
40
- def _validate_host_with_subnets(host, allowed_hosts):
42
+ def _strip_ipv6_brackets(host: str) -> str:
43
+ if host.startswith("[") and host.endswith("]"):
44
+ return host[1:-1]
45
+ return host
46
+
47
+
48
+ def _extract_ip_from_host(host: str):
49
+ """Return an :mod:`ipaddress` object for ``host`` when possible."""
50
+
51
+ candidate = _strip_ipv6_brackets(host)
41
52
  try:
42
- ip = ipaddress.ip_address(host)
53
+ return ipaddress.ip_address(candidate)
43
54
  except ValueError:
55
+ domain, _port = split_domain_port(host)
56
+ if domain and domain != host:
57
+ candidate = _strip_ipv6_brackets(domain)
58
+ try:
59
+ return ipaddress.ip_address(candidate)
60
+ except ValueError:
61
+ return None
62
+ return None
63
+
64
+
65
+ def _validate_host_with_subnets(host, allowed_hosts):
66
+ ip = _extract_ip_from_host(host)
67
+ if ip is None:
44
68
  return _original_validate_host(host, allowed_hosts)
45
69
  for pattern in allowed_hosts:
46
70
  try:
@@ -117,6 +141,18 @@ ALLOWED_HOSTS = [
117
141
  ]
118
142
 
119
143
 
144
+ _DEFAULT_PORTS = {"http": "80", "https": "443"}
145
+
146
+
147
+ def _get_allowed_hosts() -> list[str]:
148
+ from django.conf import settings as django_settings
149
+
150
+ configured = getattr(django_settings, "ALLOWED_HOSTS", None)
151
+ if configured is None:
152
+ return ALLOWED_HOSTS
153
+ return list(configured)
154
+
155
+
120
156
  def _iter_local_hostnames(hostname: str, fqdn: str | None = None) -> list[str]:
121
157
  """Return unique hostname variants for the current machine."""
122
158
 
@@ -152,34 +188,143 @@ for host in _iter_local_hostnames(_local_hostname, _local_fqdn):
152
188
 
153
189
  # Allow CSRF origin verification for hosts within allowed subnets.
154
190
  _original_origin_verified = CsrfViewMiddleware._origin_verified
191
+ _original_check_referer = CsrfViewMiddleware._check_referer
192
+
193
+
194
+ def _host_is_allowed(host: str, allowed_hosts: list[str]) -> bool:
195
+ if http_request.validate_host(host, allowed_hosts):
196
+ return True
197
+ domain, _port = split_domain_port(host)
198
+ if domain and domain != host:
199
+ return http_request.validate_host(domain, allowed_hosts)
200
+ return False
201
+
202
+
203
+ def _parse_forwarded_header(header_value: str) -> list[dict[str, str]]:
204
+ entries: list[dict[str, str]] = []
205
+ if not header_value:
206
+ return entries
207
+ for forwarded_part in header_value.split(","):
208
+ entry: dict[str, str] = {}
209
+ for element in forwarded_part.split(";"):
210
+ if "=" not in element:
211
+ continue
212
+ key, value = element.split("=", 1)
213
+ entry[key.strip().lower()] = value.strip().strip('"')
214
+ if entry:
215
+ entries.append(entry)
216
+ return entries
217
+
218
+
219
+ def _get_request_scheme(request, forwarded_entry: dict[str, str] | None = None) -> str:
220
+ """Return the scheme used by the client, honoring proxy headers."""
221
+
222
+ if forwarded_entry and forwarded_entry.get("proto", "").lower() in {"http", "https"}:
223
+ return forwarded_entry["proto"].lower()
224
+
225
+ if request.is_secure():
226
+ return "https"
227
+
228
+ forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
229
+ if forwarded_proto:
230
+ candidate = forwarded_proto.split(",")[0].strip().lower()
231
+ if candidate in {"http", "https"}:
232
+ return candidate
233
+
234
+ forwarded_header = request.META.get("HTTP_FORWARDED", "")
235
+ for forwarded_entry in _parse_forwarded_header(forwarded_header):
236
+ candidate = forwarded_entry.get("proto", "").lower()
237
+ if candidate in {"http", "https"}:
238
+ return candidate
239
+
240
+ return "http"
241
+
242
+
243
+ def _normalize_origin_tuple(scheme: str | None, host: str) -> tuple[str, str, str | None] | None:
244
+ if not scheme or scheme.lower() not in {"http", "https"}:
245
+ return None
246
+ domain, port = split_domain_port(host)
247
+ normalized_host = _strip_ipv6_brackets(domain.strip().lower())
248
+ if not normalized_host:
249
+ return None
250
+ normalized_port = port.strip() if isinstance(port, str) else port
251
+ if not normalized_port:
252
+ normalized_port = _DEFAULT_PORTS.get(scheme.lower())
253
+ if normalized_port is not None:
254
+ normalized_port = str(normalized_port)
255
+ return scheme.lower(), normalized_host, normalized_port
256
+
257
+
258
+ def _normalized_request_origin(origin: str) -> tuple[str, str, str | None] | None:
259
+ parsed = urlsplit(origin)
260
+ if not parsed.scheme or not parsed.hostname:
261
+ return None
262
+ scheme = parsed.scheme.lower()
263
+ host = parsed.hostname.lower()
264
+ port = str(parsed.port) if parsed.port is not None else _DEFAULT_PORTS.get(scheme)
265
+ return scheme, host, port
266
+
267
+
268
+ def _candidate_origin_tuples(request, allowed_hosts: list[str]) -> list[tuple[str, str, str | None]]:
269
+ default_scheme = _get_request_scheme(request)
270
+ candidates: list[tuple[str, str, str | None]] = []
271
+ seen: set[tuple[str, str, str | None]] = set()
272
+
273
+ def _append_candidate(scheme: str | None, host: str) -> None:
274
+ if not scheme or not host:
275
+ return
276
+ normalized = _normalize_origin_tuple(scheme, host)
277
+ if normalized is None:
278
+ return
279
+ if not _host_is_allowed(host, allowed_hosts):
280
+ return
281
+ if normalized in seen:
282
+ return
283
+ candidates.append(normalized)
284
+ seen.add(normalized)
155
285
 
286
+ forwarded_header = request.META.get("HTTP_FORWARDED", "")
287
+ for forwarded_entry in _parse_forwarded_header(forwarded_header):
288
+ host = forwarded_entry.get("host", "").strip()
289
+ scheme = _get_request_scheme(request, forwarded_entry)
290
+ _append_candidate(scheme, host)
291
+
292
+ forwarded_host = request.META.get("HTTP_X_FORWARDED_HOST", "")
293
+ if forwarded_host:
294
+ host = forwarded_host.split(",")[0].strip()
295
+ _append_candidate(default_scheme, host)
156
296
 
157
- def _origin_verified_with_subnets(self, request):
158
- request_origin = request.META["HTTP_ORIGIN"]
159
297
  try:
160
298
  good_host = request.get_host()
161
299
  except DisallowedHost:
162
- pass
163
- else:
164
- good_origin = "%s://%s" % (
165
- "https" if request.is_secure() else "http",
166
- good_host,
167
- )
168
- if request_origin == good_origin:
300
+ good_host = ""
301
+ if good_host:
302
+ _append_candidate(default_scheme, good_host)
303
+
304
+ return candidates
305
+
306
+
307
+ def _origin_verified_with_subnets(self, request):
308
+ request_origin = request.META["HTTP_ORIGIN"]
309
+ allowed_hosts = _get_allowed_hosts()
310
+ normalized_origin = _normalized_request_origin(request_origin)
311
+ if normalized_origin is None:
312
+ return _original_origin_verified(self, request)
313
+
314
+ origin_ip = _extract_ip_from_host(normalized_origin[1])
315
+
316
+ for candidate in _candidate_origin_tuples(request, allowed_hosts):
317
+ if candidate == normalized_origin:
169
318
  return True
170
- try:
171
- origin_host = urlsplit(request_origin).hostname
172
- origin_ip = ipaddress.ip_address(origin_host)
173
- request_ip = ipaddress.ip_address(good_host.split(":")[0])
174
- except ValueError:
175
- pass
176
- else:
177
- for pattern in ALLOWED_HOSTS:
319
+
320
+ candidate_ip = _extract_ip_from_host(candidate[1])
321
+ if origin_ip and candidate_ip:
322
+ for pattern in allowed_hosts:
178
323
  try:
179
324
  network = ipaddress.ip_network(pattern)
180
325
  except ValueError:
181
326
  continue
182
- if origin_ip in network and request_ip in network:
327
+ if origin_ip in network and candidate_ip in network:
183
328
  return True
184
329
  return _original_origin_verified(self, request)
185
330
 
@@ -187,6 +332,49 @@ def _origin_verified_with_subnets(self, request):
187
332
  CsrfViewMiddleware._origin_verified = _origin_verified_with_subnets
188
333
 
189
334
 
335
+ def _check_referer_with_forwarded(self, request):
336
+ referer = request.META.get("HTTP_REFERER")
337
+ if referer is None:
338
+ return _original_check_referer(self, request)
339
+
340
+ try:
341
+ parsed = urlsplit(referer)
342
+ except ValueError:
343
+ return _original_check_referer(self, request)
344
+
345
+ if "" in (parsed.scheme, parsed.netloc):
346
+ return _original_check_referer(self, request)
347
+
348
+ if parsed.scheme.lower() != "https":
349
+ return _original_check_referer(self, request)
350
+
351
+ normalized_referer = _normalize_origin_tuple(parsed.scheme.lower(), parsed.netloc)
352
+ if normalized_referer is None:
353
+ return _original_check_referer(self, request)
354
+
355
+ allowed_hosts = _get_allowed_hosts()
356
+ referer_ip = _extract_ip_from_host(normalized_referer[1])
357
+
358
+ for candidate in _candidate_origin_tuples(request, allowed_hosts):
359
+ if candidate == normalized_referer:
360
+ return
361
+
362
+ candidate_ip = _extract_ip_from_host(candidate[1])
363
+ if referer_ip and candidate_ip:
364
+ for pattern in allowed_hosts:
365
+ try:
366
+ network = ipaddress.ip_network(pattern)
367
+ except ValueError:
368
+ continue
369
+ if referer_ip in network and candidate_ip in network:
370
+ return
371
+
372
+ return _original_check_referer(self, request)
373
+
374
+
375
+ CsrfViewMiddleware._check_referer = _check_referer_with_forwarded
376
+
377
+
190
378
  # Application definition
191
379
 
192
380
  LOCAL_APPS = [
@@ -203,6 +391,8 @@ INSTALLED_APPS = [
203
391
  "whitenoise.runserver_nostatic",
204
392
  "django.contrib.admin",
205
393
  "django.contrib.admindocs",
394
+ "django_otp",
395
+ "django_otp.plugins.otp_totp",
206
396
  "config.auth_app.AuthConfig",
207
397
  "django.contrib.contenttypes",
208
398
  "django.contrib.sessions",
@@ -250,6 +440,7 @@ MIDDLEWARE = [
250
440
  "django.middleware.common.CommonMiddleware",
251
441
  "django.middleware.csrf.CsrfViewMiddleware",
252
442
  "django.contrib.auth.middleware.AuthenticationMiddleware",
443
+ "django_otp.middleware.OTPMiddleware",
253
444
  "core.middleware.AdminHistoryMiddleware",
254
445
  "core.middleware.SigilContextMiddleware",
255
446
  "pages.middleware.ViewHistoryMiddleware",
@@ -268,6 +459,9 @@ if DEBUG:
268
459
 
269
460
  CSRF_FAILURE_VIEW = "pages.views.csrf_failure"
270
461
 
462
+ # Allow staff TODO pages to embed internal admin views inside iframes.
463
+ X_FRAME_OPTIONS = "SAMEORIGIN"
464
+
271
465
  ROOT_URLCONF = "config.urls"
272
466
 
273
467
  TEMPLATES = [
@@ -326,9 +520,13 @@ AUTH_USER_MODEL = "core.User"
326
520
  # Enable RFID authentication backend and restrict default admin login to localhost
327
521
  AUTHENTICATION_BACKENDS = [
328
522
  "core.backends.LocalhostAdminBackend",
523
+ "core.backends.TOTPBackend",
329
524
  "core.backends.RFIDBackend",
330
525
  ]
331
526
 
527
+ # Issuer name used when generating otpauth URLs for authenticator apps.
528
+ OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
529
+
332
530
  # Database
333
531
  # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
334
532
 
@@ -405,10 +603,10 @@ AUTH_PASSWORD_VALIDATORS = [
405
603
  LANGUAGE_CODE = "en-us"
406
604
 
407
605
  LANGUAGES = [
408
- ("en", _("English")),
409
606
  ("es", _("Spanish")),
410
- ("fr", _("French")),
411
- ("ru", _("Russian")),
607
+ ("en", _("English")),
608
+ ("it", _("Italian")),
609
+ ("de", _("German")),
412
610
  ]
413
611
 
414
612
  LOCALE_PATHS = [BASE_DIR / "locale"]
config/urls.py CHANGED
@@ -118,11 +118,17 @@ urlpatterns = [
118
118
  admin.site.admin_view(pages_views.admin_model_graph),
119
119
  name="admin-model-graph",
120
120
  ),
121
+ path("version/", core_views.version_info, name="version-info"),
121
122
  path(
122
123
  "admin/core/releases/<int:pk>/<str:action>/",
123
124
  core_views.release_progress,
124
125
  name="release-progress",
125
126
  ),
127
+ path(
128
+ "admin/core/todos/<int:pk>/focus/",
129
+ core_views.todo_focus,
130
+ name="todo-focus",
131
+ ),
126
132
  path(
127
133
  "admin/core/todos/<int:pk>/done/",
128
134
  core_views.todo_done,