arthexis 0.1.14__py3-none-any.whl → 0.1.15__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.
- {arthexis-0.1.14.dist-info → arthexis-0.1.15.dist-info}/METADATA +4 -2
- {arthexis-0.1.14.dist-info → arthexis-0.1.15.dist-info}/RECORD +23 -22
- core/admin.py +26 -2
- core/entity.py +17 -1
- core/log_paths.py +24 -10
- core/models.py +28 -0
- core/release.py +121 -2
- core/system.py +203 -3
- core/tests.py +73 -0
- core/views.py +32 -6
- nodes/models.py +29 -2
- nodes/tests.py +23 -3
- pages/admin.py +62 -1
- pages/middleware.py +4 -0
- pages/models.py +36 -0
- pages/tasks.py +74 -0
- pages/tests.py +414 -1
- pages/urls.py +1 -0
- pages/utils.py +11 -0
- pages/views.py +45 -34
- {arthexis-0.1.14.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.15
|
|
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
|
|
7
7
|
Project-URL: Repository, https://github.com/arthexis/arthexis
|
|
@@ -118,6 +118,8 @@ Dynamic: license-file
|
|
|
118
118
|
|
|
119
119
|
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://github.com/arthexis/arthexis/blob/main/docs/development/ocpp-user-manual.md)
|
|
120
120
|
|
|
121
|
+
For coding guidance, see [AGENTS.md](AGENTS.md).
|
|
122
|
+
|
|
121
123
|
## Purpose
|
|
122
124
|
|
|
123
125
|
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)).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.15.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
|
|
@@ -15,14 +15,14 @@ config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,31
|
|
|
15
15
|
config/urls.py,sha256=ad9D3kGvv6Fem1ErYo8FtXWKFfjcxVr-6lstKekbO-0,5192
|
|
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=
|
|
18
|
+
core/admin.py,sha256=K57fxv6edej4isMM67yStYc9-2Hjm-87FolZkwiTUtM,137295
|
|
19
19
|
core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
|
|
20
20
|
core/admindocs.py,sha256=1wJkaVpOklcZnYgeksM14DoISzVpFEigeG5GUGnpji4,5216
|
|
21
21
|
core/apps.py,sha256=VavdJtQ_JIwyI0pbB8oByQLyzQnKNflH3Fobl6BxA6E,14316
|
|
22
22
|
core/auto_upgrade.py,sha256=1EffHHFylgydWdZM_id6CppV0QqBtdNw7cwBYVdbNdk,1715
|
|
23
23
|
core/backends.py,sha256=GLVJpkY6o0V0AyLVCO9BYByU9Logdz4tou6w5n9-Wx0,8838
|
|
24
24
|
core/changelog.py,sha256=grMvuEektkymwvkC1ubXFZF2JFopPybT82k4rUIlfmo,10840
|
|
25
|
-
core/entity.py,sha256=
|
|
25
|
+
core/entity.py,sha256=o4VteOXePGEsIWJFZ3fpq3DZsdWr3hpQ9A6kFbKosSE,4844
|
|
26
26
|
core/environment.py,sha256=JLcvxAwU3OTL8O6kzwcUCFNZ3T28KanHrU_4mDBFamU,1584
|
|
27
27
|
core/fields.py,sha256=d-qGahdcv4SRcO4fwCJ6_-NnEAP5xW0k3kODdAAAHSA,5412
|
|
28
28
|
core/form_fields.py,sha256=h2xT8sO8EWbznsiARkxukFk69yoW6mQwqpgonA-d6aA,2496
|
|
@@ -31,27 +31,27 @@ core/github_issues.py,sha256=tkboxXR92_Im2Mac2eU7fHtqcO-MQMdkEmFg4f6PfDc,5006
|
|
|
31
31
|
core/github_repos.py,sha256=8KCxcEiO2Ltgde7UDTAFOyHTm_eBeZYUIZegEbrjkWA,1690
|
|
32
32
|
core/lcd_screen.py,sha256=WtHMlSoZXKOsdM0d-v-f8ul-LSA6FA1bEWFwho1t6s8,2573
|
|
33
33
|
core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
|
|
34
|
-
core/log_paths.py,sha256=
|
|
34
|
+
core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
|
|
35
35
|
core/mailer.py,sha256=ciIZBJuKMJkmo5h8ktJPVlyzghzfNvhC8TGq2CSeGEA,2744
|
|
36
36
|
core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
|
|
37
|
-
core/models.py,sha256=
|
|
37
|
+
core/models.py,sha256=xt6bM3yvDhoIZf9u4GVdgj8HpDJcS-MGDWgd7qdqNdU,122902
|
|
38
38
|
core/notifications.py,sha256=LYktoKM5k4q7YYWAJuqdeKM-p0Q-3gXgfqdq71qLS68,3916
|
|
39
39
|
core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
|
|
40
40
|
core/reference_utils.py,sha256=jeox3V4cZNxzM2Jj31g_mdb3O55zy9S2iXAZu70R1Zc,3627
|
|
41
|
-
core/release.py,sha256=
|
|
41
|
+
core/release.py,sha256=ucO4-l09l8RkviHi9DAOKBfCjGnEJDcHLK9-sGcVedw,28043
|
|
42
42
|
core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,3516
|
|
43
43
|
core/sigil_builder.py,sha256=VLwbrrD7Zr3SHfIDYV-V7uv7LEGiIelCSkeGswHibuc,4843
|
|
44
44
|
core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
|
-
core/system.py,sha256=
|
|
46
|
+
core/system.py,sha256=5jdzmg236RxE51RKp1pErRJFKOvs42YJPgzVynwJooQ,28973
|
|
47
47
|
core/tasks.py,sha256=mZtVotqyNPOXvMY1tsQtkblLbvGcIxKUvQfx8nNZkd4,12421
|
|
48
48
|
core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
|
|
49
49
|
core/test_system_info.py,sha256=V9lzW9fnCFWlOoXZpGJ2aLSqjE6oexQ6lzobbTuGNJE,6371
|
|
50
|
-
core/tests.py,sha256=
|
|
50
|
+
core/tests.py,sha256=2wwwQtpOx45BixBxFI8rD9FNlDOfgdABPvoyYaTxTEk,85475
|
|
51
51
|
core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
|
|
52
52
|
core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
|
|
53
53
|
core/user_data.py,sha256=02CfvxayELWSWZJCxWpv1Yz7EGg08yEu5MM31Khsi0U,21083
|
|
54
|
-
core/views.py,sha256=
|
|
54
|
+
core/views.py,sha256=udz_YhyPi3WF3cGcl_9VfliUtHdXaeLNf3HBXJIdhMo,78135
|
|
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
|
|
@@ -62,12 +62,12 @@ nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
|
|
|
62
62
|
nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
|
|
63
63
|
nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
|
|
64
64
|
nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
|
|
65
|
-
nodes/models.py,sha256=
|
|
65
|
+
nodes/models.py,sha256=1GFx9ZSJqRBecIn0ywkTG5nRQIuIeArJCoA6Q66TtHQ,60349
|
|
66
66
|
nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
|
|
67
67
|
nodes/rfid_sync.py,sha256=754u-Di1Fzond1LQq4i7mJAcTPRgUwsdKk3Dz5Ba1Dw,6371
|
|
68
68
|
nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
|
|
69
69
|
nodes/tasks.py,sha256=ur59ebu9z02idmvy_IvUQt3eu9LWRyyNpkg2szvIHCQ,1522
|
|
70
|
-
nodes/tests.py,sha256=
|
|
70
|
+
nodes/tests.py,sha256=34-56EgVdvelepzChRuhFaGhpQY-a2uH8iwczvXRMrY,150638
|
|
71
71
|
nodes/urls.py,sha256=HmAxj6sr6nMf0lii_1UX7sNBJUcrkaiKm3R9ofUWhvM,677
|
|
72
72
|
nodes/utils.py,sha256=3Vtjsi5nPvKqI0bdu6dabGJlOZ6ybkeIyFiJ7JPumdM,4406
|
|
73
73
|
nodes/views.py,sha256=TyW7exkVaR-o2_XkJXSi9jQ_BygXOE2cQFs4xlI20Xc,22905
|
|
@@ -91,19 +91,20 @@ ocpp/transactions_io.py,sha256=YnxI-Tv5UFxv0JuFK3XpoqFYP8eRT8sMuDiqkiMHPtU,7387
|
|
|
91
91
|
ocpp/urls.py,sha256=3T5O5DSwVk4PbhPx5p4D3UseCWvC5xV5HwJLSM6AfA8,1700
|
|
92
92
|
ocpp/views.py,sha256=LE2mqB5FTno4SYzBWabu9g95o77Ojo2uFtTG6K5W9F0,56311
|
|
93
93
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
|
-
pages/admin.py,sha256=
|
|
94
|
+
pages/admin.py,sha256=vbUP8JvjDbVS6OtfSZwYzlwyEy_jYBraG8zwSXpl4Js,23353
|
|
95
95
|
pages/apps.py,sha256=AzUNXQX0yRUX5jus-5EDReDb0nOEY8DBgYaM970u6Io,288
|
|
96
96
|
pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
97
97
|
pages/context_processors.py,sha256=8TmtbbXsX0sbkT-_kOfpINzJhUpZbLy9tMHWLVm7n9s,4277
|
|
98
98
|
pages/defaults.py,sha256=l36APPAZO4ub2A8Pp-lQGujKeOVYcyzU6t7-kOk8VoA,522
|
|
99
99
|
pages/forms.py,sha256=T0atqxdNds3IBP8N-9c5-ACf3iR9FzzmhzK4MOa24e8,7058
|
|
100
|
-
pages/middleware.py,sha256=
|
|
101
|
-
pages/models.py,sha256=
|
|
102
|
-
pages/
|
|
103
|
-
pages/
|
|
104
|
-
pages/
|
|
105
|
-
pages/
|
|
106
|
-
|
|
107
|
-
arthexis-0.1.
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
100
|
+
pages/middleware.py,sha256=6PMLiyuHAHbfLeHwwQxIVy2fJ32ramEO9SHAN05Set4,6967
|
|
101
|
+
pages/models.py,sha256=YMoKDgVGrEKizSpxtsZLDl7zC9ayVgQM4BtZOTmG6SM,20910
|
|
102
|
+
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
103
|
+
pages/tests.py,sha256=LJ0uVkQ0v_c2oGBdwEp_CQ8NgcIDrq9fzLSaM8uDB0A,117176
|
|
104
|
+
pages/urls.py,sha256=tnT6h4Zb5whuChvhfpkOF1UDOJu6jCwQk72FguUN9SU,1092
|
|
105
|
+
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
106
|
+
pages/views.py,sha256=JkKy6w-xFySG_T1UCnQk3ooq0cijGHUH5YhSivyGaG4,43616
|
|
107
|
+
arthexis-0.1.15.dist-info/METADATA,sha256=WEu-5YYSs9HX-ZisdIGvIcvj7JNmmWKA3tdpWKCFaMg,10047
|
|
108
|
+
arthexis-0.1.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
109
|
+
arthexis-0.1.15.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
110
|
+
arthexis-0.1.15.dist-info/RECORD,,
|
core/admin.py
CHANGED
|
@@ -424,6 +424,7 @@ class ReleaseManagerAdminForm(forms.ModelForm):
|
|
|
424
424
|
widgets = {
|
|
425
425
|
"pypi_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
426
426
|
"github_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
427
|
+
"git_password": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
427
428
|
}
|
|
428
429
|
|
|
429
430
|
def __init__(self, *args, **kwargs):
|
|
@@ -448,6 +449,16 @@ class ReleaseManagerAdminForm(forms.ModelForm):
|
|
|
448
449
|
"or an equivalent fine-grained token) and paste it here."
|
|
449
450
|
),
|
|
450
451
|
)
|
|
452
|
+
self.fields["git_username"].help_text = (
|
|
453
|
+
"Username used for HTTPS git pushes (for example, your GitHub username)."
|
|
454
|
+
)
|
|
455
|
+
self.fields["git_password"].help_text = format_html(
|
|
456
|
+
"{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
|
|
457
|
+
"Provide the password or personal access token used for pushing tags. ",
|
|
458
|
+
"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token",
|
|
459
|
+
"docs.github.com/.../creating-a-personal-access-token",
|
|
460
|
+
" If left blank, the GitHub token will be used instead.",
|
|
461
|
+
)
|
|
451
462
|
|
|
452
463
|
|
|
453
464
|
@admin.register(ReleaseManager)
|
|
@@ -460,18 +471,27 @@ class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModel
|
|
|
460
471
|
fieldsets = (
|
|
461
472
|
("Owner", {"fields": ("user", "group")}),
|
|
462
473
|
(
|
|
463
|
-
"
|
|
474
|
+
"PyPI",
|
|
464
475
|
{
|
|
465
476
|
"fields": (
|
|
466
477
|
"pypi_username",
|
|
467
478
|
"pypi_token",
|
|
468
479
|
"pypi_password",
|
|
469
|
-
"github_token",
|
|
470
480
|
"pypi_url",
|
|
471
481
|
"secondary_pypi_url",
|
|
472
482
|
)
|
|
473
483
|
},
|
|
474
484
|
),
|
|
485
|
+
(
|
|
486
|
+
"GitHub",
|
|
487
|
+
{
|
|
488
|
+
"fields": (
|
|
489
|
+
"github_token",
|
|
490
|
+
"git_username",
|
|
491
|
+
"git_password",
|
|
492
|
+
)
|
|
493
|
+
},
|
|
494
|
+
),
|
|
475
495
|
)
|
|
476
496
|
|
|
477
497
|
def owner(self, obj):
|
|
@@ -1249,12 +1269,16 @@ class ReleaseManagerInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
1249
1269
|
"pypi_username",
|
|
1250
1270
|
"pypi_token",
|
|
1251
1271
|
"github_token",
|
|
1272
|
+
"git_username",
|
|
1273
|
+
"git_password",
|
|
1252
1274
|
"pypi_password",
|
|
1253
1275
|
"pypi_url",
|
|
1276
|
+
"secondary_pypi_url",
|
|
1254
1277
|
)
|
|
1255
1278
|
widgets = {
|
|
1256
1279
|
"pypi_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
1257
1280
|
"github_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
1281
|
+
"git_password": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
1258
1282
|
}
|
|
1259
1283
|
|
|
1260
1284
|
|
core/entity.py
CHANGED
|
@@ -4,10 +4,14 @@ import logging
|
|
|
4
4
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
|
5
5
|
from django.core.exceptions import FieldDoesNotExist
|
|
6
6
|
from django.db import models
|
|
7
|
+
from django.dispatch import Signal
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
10
11
|
|
|
12
|
+
user_data_flag_updated = Signal()
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
class EntityQuerySet(models.QuerySet):
|
|
12
16
|
def delete(self): # pragma: no cover - delegates to instance delete
|
|
13
17
|
deleted = 0
|
|
@@ -16,12 +20,24 @@ class EntityQuerySet(models.QuerySet):
|
|
|
16
20
|
deleted += 1
|
|
17
21
|
return deleted, {}
|
|
18
22
|
|
|
23
|
+
def update(self, **kwargs):
|
|
24
|
+
invalidate_user_data_cache = "is_user_data" in kwargs
|
|
25
|
+
updated = super().update(**kwargs)
|
|
26
|
+
if invalidate_user_data_cache and updated:
|
|
27
|
+
user_data_flag_updated.send(sender=self.model)
|
|
28
|
+
return updated
|
|
29
|
+
|
|
19
30
|
|
|
20
31
|
class EntityManager(models.Manager):
|
|
21
32
|
def get_queryset(self):
|
|
22
33
|
return EntityQuerySet(self.model, using=self._db).filter(is_deleted=False)
|
|
23
34
|
|
|
24
35
|
|
|
36
|
+
class EntityAllManager(models.Manager):
|
|
37
|
+
def get_queryset(self):
|
|
38
|
+
return EntityQuerySet(self.model, using=self._db)
|
|
39
|
+
|
|
40
|
+
|
|
25
41
|
class EntityUserManager(DjangoUserManager):
|
|
26
42
|
def get_queryset(self):
|
|
27
43
|
return EntityQuerySet(self.model, using=self._db).filter(is_deleted=False)
|
|
@@ -35,7 +51,7 @@ class Entity(models.Model):
|
|
|
35
51
|
is_deleted = models.BooleanField(default=False, editable=False)
|
|
36
52
|
|
|
37
53
|
objects = EntityManager()
|
|
38
|
-
all_objects =
|
|
54
|
+
all_objects = EntityAllManager()
|
|
39
55
|
|
|
40
56
|
class Meta:
|
|
41
57
|
abstract = True
|
core/log_paths.py
CHANGED
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
+
import tempfile
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def _is_root() -> bool:
|
|
@@ -41,16 +42,29 @@ def select_log_dir(base_dir: Path) -> Path:
|
|
|
41
42
|
candidates.append(Path("/var/log/arthexis"))
|
|
42
43
|
candidates.append(Path("/tmp/arthexis/logs"))
|
|
43
44
|
else:
|
|
44
|
-
home
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
home: Path | None
|
|
46
|
+
try:
|
|
47
|
+
home = Path.home()
|
|
48
|
+
except (RuntimeError, OSError, KeyError):
|
|
49
|
+
home = None
|
|
50
|
+
|
|
51
|
+
candidates.append(default)
|
|
52
|
+
|
|
53
|
+
tmp_logs = Path(tempfile.gettempdir()) / "arthexis" / "logs"
|
|
54
|
+
|
|
55
|
+
if home is not None:
|
|
56
|
+
state_home = _state_home(home)
|
|
57
|
+
candidates.extend(
|
|
58
|
+
[
|
|
59
|
+
state_home / "arthexis" / "logs",
|
|
60
|
+
home / ".arthexis" / "logs",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
candidates.append(tmp_logs)
|
|
65
|
+
|
|
66
|
+
candidates.append(Path("/tmp/arthexis/logs"))
|
|
67
|
+
candidates.append(tmp_logs)
|
|
54
68
|
|
|
55
69
|
seen: set[Path] = set()
|
|
56
70
|
ordered_candidates: list[Path] = []
|
core/models.py
CHANGED
|
@@ -50,6 +50,7 @@ from .release import (
|
|
|
50
50
|
Credentials,
|
|
51
51
|
DEFAULT_PACKAGE,
|
|
52
52
|
RepositoryTarget,
|
|
53
|
+
GitCredentials,
|
|
53
54
|
)
|
|
54
55
|
|
|
55
56
|
|
|
@@ -3024,6 +3025,8 @@ class ReleaseManager(Profile):
|
|
|
3024
3025
|
"pypi_username",
|
|
3025
3026
|
"pypi_token",
|
|
3026
3027
|
"github_token",
|
|
3028
|
+
"git_username",
|
|
3029
|
+
"git_password",
|
|
3027
3030
|
"pypi_password",
|
|
3028
3031
|
"pypi_url",
|
|
3029
3032
|
"secondary_pypi_url",
|
|
@@ -3038,6 +3041,21 @@ class ReleaseManager(Profile):
|
|
|
3038
3041
|
"Used before the GITHUB_TOKEN environment variable."
|
|
3039
3042
|
),
|
|
3040
3043
|
)
|
|
3044
|
+
git_username = SigilShortAutoField(
|
|
3045
|
+
"Git username",
|
|
3046
|
+
max_length=100,
|
|
3047
|
+
blank=True,
|
|
3048
|
+
help_text="Username used for Git pushes (for example, your GitHub username).",
|
|
3049
|
+
)
|
|
3050
|
+
git_password = SigilShortAutoField(
|
|
3051
|
+
"Git password/token",
|
|
3052
|
+
max_length=200,
|
|
3053
|
+
blank=True,
|
|
3054
|
+
help_text=(
|
|
3055
|
+
"Password or personal access token for HTTPS Git pushes. "
|
|
3056
|
+
"Leave blank to use the GitHub token instead."
|
|
3057
|
+
),
|
|
3058
|
+
)
|
|
3041
3059
|
pypi_password = SigilShortAutoField("PyPI password", max_length=200, blank=True)
|
|
3042
3060
|
pypi_url = SigilShortAutoField("PyPI URL", max_length=200, blank=True)
|
|
3043
3061
|
secondary_pypi_url = SigilShortAutoField(
|
|
@@ -3079,6 +3097,16 @@ class ReleaseManager(Profile):
|
|
|
3079
3097
|
return Credentials(username=self.pypi_username, password=self.pypi_password)
|
|
3080
3098
|
return None
|
|
3081
3099
|
|
|
3100
|
+
def to_git_credentials(self) -> GitCredentials | None:
|
|
3101
|
+
"""Return Git credentials for pushing tags."""
|
|
3102
|
+
|
|
3103
|
+
username = (self.git_username or "").strip()
|
|
3104
|
+
password_source = self.git_password or self.github_token or ""
|
|
3105
|
+
password = password_source.strip()
|
|
3106
|
+
if username and password:
|
|
3107
|
+
return GitCredentials(username=username, password=password)
|
|
3108
|
+
return None
|
|
3109
|
+
|
|
3082
3110
|
|
|
3083
3111
|
class Package(Entity):
|
|
3084
3112
|
"""Package details shared across releases."""
|
core/release.py
CHANGED
|
@@ -10,6 +10,7 @@ import time
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Optional, Sequence
|
|
13
|
+
from urllib.parse import quote, urlsplit, urlunsplit
|
|
13
14
|
|
|
14
15
|
try: # pragma: no cover - optional dependency
|
|
15
16
|
import toml # type: ignore
|
|
@@ -70,6 +71,17 @@ class Credentials:
|
|
|
70
71
|
raise ValueError("Missing PyPI credentials")
|
|
71
72
|
|
|
72
73
|
|
|
74
|
+
@dataclass
|
|
75
|
+
class GitCredentials:
|
|
76
|
+
"""Credentials used for Git operations such as pushing tags."""
|
|
77
|
+
|
|
78
|
+
username: Optional[str] = None
|
|
79
|
+
password: Optional[str] = None
|
|
80
|
+
|
|
81
|
+
def has_auth(self) -> bool:
|
|
82
|
+
return bool((self.username or "").strip() and (self.password or "").strip())
|
|
83
|
+
|
|
84
|
+
|
|
73
85
|
@dataclass
|
|
74
86
|
class RepositoryTarget:
|
|
75
87
|
"""Configuration for uploading a distribution to a repository."""
|
|
@@ -90,7 +102,7 @@ class RepositoryTarget:
|
|
|
90
102
|
|
|
91
103
|
DEFAULT_PACKAGE = Package(
|
|
92
104
|
name="arthexis",
|
|
93
|
-
description="
|
|
105
|
+
description="Power & Energy Infrastructure",
|
|
94
106
|
author="Rafael J. Guillén-Osorio",
|
|
95
107
|
email="tecnologia@gelectriic.com",
|
|
96
108
|
python_requires=">=3.10",
|
|
@@ -243,6 +255,113 @@ def _manager_credentials() -> Optional[Credentials]:
|
|
|
243
255
|
return None
|
|
244
256
|
|
|
245
257
|
|
|
258
|
+
def _manager_git_credentials(package: Optional[Package] = None) -> Optional[GitCredentials]:
|
|
259
|
+
"""Return Git credentials from the Package's release manager if available."""
|
|
260
|
+
|
|
261
|
+
try: # pragma: no cover - optional dependency
|
|
262
|
+
from core.models import Package as PackageModel
|
|
263
|
+
|
|
264
|
+
queryset = PackageModel.objects.select_related("release_manager")
|
|
265
|
+
if package is not None:
|
|
266
|
+
queryset = queryset.filter(name=package.name)
|
|
267
|
+
package_obj = queryset.first()
|
|
268
|
+
if package_obj and package_obj.release_manager:
|
|
269
|
+
creds = package_obj.release_manager.to_git_credentials()
|
|
270
|
+
if creds and creds.has_auth():
|
|
271
|
+
return creds
|
|
272
|
+
except Exception:
|
|
273
|
+
return None
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _git_authentication_missing(exc: subprocess.CalledProcessError) -> bool:
|
|
278
|
+
message = (exc.stderr or exc.stdout or "").strip().lower()
|
|
279
|
+
if not message:
|
|
280
|
+
return False
|
|
281
|
+
auth_markers = [
|
|
282
|
+
"could not read username",
|
|
283
|
+
"authentication failed",
|
|
284
|
+
"fatal: authentication failed",
|
|
285
|
+
"terminal prompts disabled",
|
|
286
|
+
]
|
|
287
|
+
return any(marker in message for marker in auth_markers)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _format_subprocess_error(exc: subprocess.CalledProcessError) -> str:
|
|
291
|
+
return (exc.stderr or exc.stdout or str(exc)).strip() or str(exc)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _git_remote_url(remote: str = "origin") -> Optional[str]:
|
|
295
|
+
proc = subprocess.run(
|
|
296
|
+
["git", "remote", "get-url", remote],
|
|
297
|
+
capture_output=True,
|
|
298
|
+
text=True,
|
|
299
|
+
check=False,
|
|
300
|
+
)
|
|
301
|
+
if proc.returncode != 0:
|
|
302
|
+
return None
|
|
303
|
+
return (proc.stdout or "").strip() or None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _remote_with_credentials(url: str, creds: GitCredentials) -> Optional[str]:
|
|
307
|
+
if not creds.has_auth():
|
|
308
|
+
return None
|
|
309
|
+
parsed = urlsplit(url)
|
|
310
|
+
if parsed.scheme not in {"http", "https"}:
|
|
311
|
+
return None
|
|
312
|
+
host = parsed.netloc.split("@", 1)[-1]
|
|
313
|
+
username = quote((creds.username or "").strip(), safe="")
|
|
314
|
+
password = quote((creds.password or "").strip(), safe="")
|
|
315
|
+
netloc = f"{username}:{password}@{host}"
|
|
316
|
+
return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _raise_git_authentication_error(tag_name: str, exc: subprocess.CalledProcessError) -> None:
|
|
320
|
+
details = _format_subprocess_error(exc)
|
|
321
|
+
message = (
|
|
322
|
+
"Git authentication failed while pushing tag {tag}. "
|
|
323
|
+
"Configure Git credentials in the release manager profile or authenticate "
|
|
324
|
+
"locally, then rerun the publish step or push the tag manually with `git push "
|
|
325
|
+
"origin {tag}`."
|
|
326
|
+
).format(tag=tag_name)
|
|
327
|
+
if details:
|
|
328
|
+
message = f"{message} Git reported: {details}"
|
|
329
|
+
raise ReleaseError(message) from exc
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _push_tag(tag_name: str, package: Package) -> None:
|
|
333
|
+
auth_error: subprocess.CalledProcessError | None = None
|
|
334
|
+
try:
|
|
335
|
+
_run(["git", "push", "origin", tag_name])
|
|
336
|
+
return
|
|
337
|
+
except subprocess.CalledProcessError as exc:
|
|
338
|
+
if not _git_authentication_missing(exc):
|
|
339
|
+
raise
|
|
340
|
+
auth_error = exc
|
|
341
|
+
|
|
342
|
+
creds = _manager_git_credentials(package)
|
|
343
|
+
if creds and creds.has_auth():
|
|
344
|
+
remote_url = _git_remote_url("origin")
|
|
345
|
+
if remote_url:
|
|
346
|
+
authed_url = _remote_with_credentials(remote_url, creds)
|
|
347
|
+
if authed_url:
|
|
348
|
+
try:
|
|
349
|
+
_run(["git", "push", authed_url, tag_name])
|
|
350
|
+
return
|
|
351
|
+
except subprocess.CalledProcessError as push_exc:
|
|
352
|
+
if not _git_authentication_missing(push_exc):
|
|
353
|
+
raise
|
|
354
|
+
auth_error = push_exc
|
|
355
|
+
# If we reach this point, the original exception is an auth error
|
|
356
|
+
if auth_error is not None:
|
|
357
|
+
_raise_git_authentication_error(tag_name, auth_error)
|
|
358
|
+
raise ReleaseError(
|
|
359
|
+
"Git authentication failed while pushing tag {tag}. Configure Git credentials and try again.".format(
|
|
360
|
+
tag=tag_name
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
246
365
|
def run_tests(
|
|
247
366
|
log_path: Optional[Path] = None,
|
|
248
367
|
command: Optional[Sequence[str]] = None,
|
|
@@ -541,7 +660,7 @@ def publish(
|
|
|
541
660
|
|
|
542
661
|
tag_name = f"v{version}"
|
|
543
662
|
_run(["git", "tag", tag_name])
|
|
544
|
-
|
|
663
|
+
_push_tag(tag_name, package)
|
|
545
664
|
return uploaded
|
|
546
665
|
|
|
547
666
|
|