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.
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/METADATA +63 -20
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/RECORD +39 -36
- config/settings.py +221 -23
- config/urls.py +6 -0
- core/admin.py +401 -35
- core/apps.py +3 -0
- core/auto_upgrade.py +57 -0
- core/backends.py +77 -3
- core/fields.py +93 -0
- core/models.py +212 -7
- core/reference_utils.py +97 -0
- core/sigil_builder.py +16 -3
- core/system.py +157 -143
- core/tasks.py +151 -8
- core/test_system_info.py +37 -1
- core/tests.py +288 -12
- core/user_data.py +103 -8
- core/views.py +257 -15
- nodes/admin.py +12 -4
- nodes/backends.py +109 -17
- nodes/models.py +205 -2
- nodes/tests.py +370 -1
- nodes/views.py +140 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +252 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +49 -7
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/tests.py +384 -8
- ocpp/views.py +101 -76
- pages/context_processors.py +20 -0
- pages/forms.py +131 -0
- pages/tests.py +434 -13
- pages/urls.py +1 -0
- pages/views.py +334 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.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.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
|
+
[](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
|
-
-
|
|
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
|
-
##
|
|
137
|
+
## Role Architecture
|
|
129
138
|
|
|
130
139
|
Arthexis Constellation ships in four node roles tailored to different deployment scenarios.
|
|
131
140
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
<table border="1" cellpadding="8" cellspacing="0">
|
|
142
|
+
<thead>
|
|
143
|
+
<tr>
|
|
144
|
+
<th align="left">Role</th>
|
|
145
|
+
<th align="left">Description & 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 & 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 & 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 & 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 & 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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
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
|
-
|
|
155
|
-
-
|
|
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:
|
|
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.
|
|
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=
|
|
14
|
-
config/urls.py,sha256
|
|
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=
|
|
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=
|
|
21
|
-
core/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
40
|
-
core/tasks.py,sha256=
|
|
41
|
-
core/test_system_info.py,sha256=
|
|
42
|
-
core/tests.py,sha256=
|
|
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=
|
|
46
|
-
core/views.py,sha256=
|
|
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=
|
|
54
|
+
nodes/admin.py,sha256=o0iiNzHE7fNv8bpj7VAdTGboT05hVCCtrTdknAzevZE,16736
|
|
53
55
|
nodes/apps.py,sha256=KijGydsQBS-8Q8uBc0ahDZXSSCMzP3tQcYIEigJxdCE,2077
|
|
54
|
-
nodes/backends.py,sha256=
|
|
56
|
+
nodes/backends.py,sha256=dQMbC-T9vSxvXMxssi0Y2ZKmc8LbSM_h4lm3EDbtZeo,5525
|
|
55
57
|
nodes/lcd.py,sha256=7MqS3jV_De2ysSmTtXQfYTWaxdfbmm6YfNdb_7qIVZc,5923
|
|
56
|
-
nodes/models.py,sha256=
|
|
58
|
+
nodes/models.py,sha256=mvryiS3S3ZT0sOpANGJYzA1P3W00kT-InIgXzwFFsao,37629
|
|
57
59
|
nodes/tasks.py,sha256=jorbN4h0PqWMBRMFdkGgJtvG8GPFwGr5Hxlm5In3EeY,1568
|
|
58
|
-
nodes/tests.py,sha256=
|
|
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=
|
|
63
|
+
nodes/views.py,sha256=qBEu19h64cTyn9YuuL9YRnqymCdZPsmMbjXedjd9fzY,14988
|
|
62
64
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
|
-
ocpp/admin.py,sha256=
|
|
65
|
+
ocpp/admin.py,sha256=Rw0PinweVSaBjIQIcgj_cgfalNRBHcrAh3JInx8Hn7A,17129
|
|
64
66
|
ocpp/apps.py,sha256=mCZ5Z0ei7z7c62luIcIhbEuwL1N4czQFSToOkGvRmms,867
|
|
65
|
-
ocpp/consumers.py,sha256=
|
|
66
|
-
ocpp/evcs.py,sha256=
|
|
67
|
-
ocpp/models.py,sha256=
|
|
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=
|
|
70
|
-
ocpp/store.py,sha256=
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
86
|
-
pages/urls.py,sha256=
|
|
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=
|
|
89
|
-
arthexis-0.1.
|
|
90
|
-
arthexis-0.1.
|
|
91
|
-
arthexis-0.1.
|
|
92
|
-
arthexis-0.1.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
("
|
|
411
|
-
("
|
|
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,
|