arthexis 0.1.16__py3-none-any.whl → 0.1.17__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.16
3
+ Version: 0.1.17
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
@@ -1,4 +1,4 @@
1
- arthexis-0.1.16.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
1
+ arthexis-0.1.17.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,18 +10,18 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
10
10
  config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
11
11
  config/middleware.py,sha256=mDU5tye8H4WCjpJqocwd0vmrzoVEYwdz9WTP4Hcr6dI,719
12
12
  config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
13
- config/settings.py,sha256=2ANaLD4_Vq3E84rDA2ulqK_DT_hu89Zj4ED5FVEjPBA,21427
13
+ config/settings.py,sha256=WFywFlWRTkEqDksWYAOd6DdSpHLEu2ETgLeeSGruwrA,21516
14
14
  config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
15
15
  config/urls.py,sha256=lXl2KKsbIehjOW0W6FHAsxkZJ-3DAo37f2ICb1dvvz8,5320
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=c3Z5UE3cBXtJR6pQcLQYR0BWe6W7SBNvR0W9lG8b2ZU,143397
18
+ core/admin.py,sha256=tEMop2REJ-P4o73ZWKZQuWm29zF3S-dWKu26wUDfvi4,145556
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=y12jggdsn90bqBDW8L0kGy2lS8-P8Ym2PmPJ8AiPjYc,10343
24
- core/changelog.py,sha256=grMvuEektkymwvkC1ubXFZF2JFopPybT82k4rUIlfmo,10840
23
+ core/backends.py,sha256=Mzv_0YYF3iNWYAaKAJHHk75X2im-Kihu1zsg8eBeW2c,10509
24
+ core/changelog.py,sha256=SRn37i5N-qb-RYV4Gpu9fg7Kv8gu4TH8ZwEmDRgN-Vo,12594
25
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
@@ -34,24 +34,24 @@ 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=cVfk96ujtn7Re7YiHm8CU1Q_URrDjJdsEwi2-3vWyWE,124680
37
+ core/models.py,sha256=Vl_Z_tLo93w2ZDgObFFIfLSzid3KXraByghMmJeCfNk,126647
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=tcRddwl0_TugcmlGlTB_7gPzICfAK2wjGtdWLVg_eaE,29756
41
+ core/release.py,sha256=y5NRs0XwB7RQVvMEZoNWYjTBxuG68dOMizUXLRx7-x8,31561
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=KRIvgEr0XwzdWuvLXmOsK43WRjPoDEuOvPn2nhVz0s4,35704
47
- core/tasks.py,sha256=PiZ5qKngXP8Q3rVEn_l9zCQ-9tx4Z8v7-t0l5fHnMvA,12535
46
+ core/system.py,sha256=tqx8-4kyViMGKI3EAaxztrbyes4TSTPQ9YsIKzdVs6c,35731
47
+ core/tasks.py,sha256=MtijKTtRHUEsTP4nVJFYx5B8Ls8EXmtzpBuq8FU5b9s,12302
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=J1ttP5u3UdU242NBoYIfbnpFVzJsmLNI671ChgQQuik,93782
50
+ core/tests.py,sha256=ojHab5JtcHuVDc1zXK_QVms1cID1XXjuRcHmPcqVqD0,98368
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=N47qQFwSSm0gCKRf2oKJIrYVESt0BIIqnTNh4wMlf6s,85237
54
+ core/views.py,sha256=Gt2J51RyIOsR2gzl2q3ChPbbIVDzrscty3yIGRW07P8,87141
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
@@ -72,7 +72,7 @@ nodes/urls.py,sha256=HmAxj6sr6nMf0lii_1UX7sNBJUcrkaiKm3R9ofUWhvM,677
72
72
  nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
73
73
  nodes/views.py,sha256=TyW7exkVaR-o2_XkJXSi9jQ_BygXOE2cQFs4xlI20Xc,22905
74
74
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- ocpp/admin.py,sha256=9rmECU832lpD7Hcb4_6pkLmQ1D_y9Q6Xl9n8pa3ux8c,31449
75
+ ocpp/admin.py,sha256=gMxHkT5KSp4kPWJcDJ1Y65VqgrwFTZl8Y516FO8oi3g,34658
76
76
  ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
77
77
  ocpp/consumers.py,sha256=LgplrJQOfs8CKCtmBcRQLcDVB4Tz7YZpb3I7r2lAorQ,66352
78
78
  ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
@@ -85,11 +85,11 @@ ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
85
85
  ocpp/store.py,sha256=rHrP2Iq2ycMFbal1UEJVXb7r4gDtI5yifaE3nT0tjJw,18855
86
86
  ocpp/tasks.py,sha256=OxIaI4OSLz9AfwLexnXhiBILBimTs3gVrPd197Jguqg,5819
87
87
  ocpp/test_export_import.py,sha256=Zp6xUBlRq7XkdKjOs78BhkujNQdklxi4RLxU8c-udWY,4530
88
- ocpp/test_rfid.py,sha256=1DeIfE9diIOV8kJoVH_5HYLOpv6GWQt7_SbZfFlpBZw,34690
89
- ocpp/tests.py,sha256=2pa6Vw5aMe-ak3LthHyo5jJmibD8I_BpFOapBwWkEdo,177608
88
+ ocpp/test_rfid.py,sha256=0Zczbg1x_5vhIV5TITHeaUkNONMdx20pD4St7Zc-ghM,36524
89
+ ocpp/tests.py,sha256=BfOapD5vWrmA43Q4WI7or2lP2pmcBdrU5_YCK31JgHE,182621
90
90
  ocpp/transactions_io.py,sha256=YnxI-Tv5UFxv0JuFK3XpoqFYP8eRT8sMuDiqkiMHPtU,7387
91
91
  ocpp/urls.py,sha256=3T5O5DSwVk4PbhPx5p4D3UseCWvC5xV5HwJLSM6AfA8,1700
92
- ocpp/views.py,sha256=LE2mqB5FTno4SYzBWabu9g95o77Ojo2uFtTG6K5W9F0,56311
92
+ ocpp/views.py,sha256=wntIO3LHFoPAg40SFGMoRPAiA3xyDKPwNgIsCBSEOcI,57164
93
93
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
94
  pages/admin.py,sha256=f2IYr-nGg9FmafQfDmIRrv01UuXh4mdhFJbnw-ytzHU,27459
95
95
  pages/apps.py,sha256=AzUNXQX0yRUX5jus-5EDReDb0nOEY8DBgYaM970u6Io,288
@@ -101,11 +101,11 @@ pages/middleware.py,sha256=6PMLiyuHAHbfLeHwwQxIVy2fJ32ramEO9SHAN05Set4,6967
101
101
  pages/models.py,sha256=Sp8e2VB5a7yg4eSUlz_VcsSlAuDVap26xBKYYggxmLM,20952
102
102
  pages/module_defaults.py,sha256=R8n6eQDjNRMpO-DW86OFGvyRarju5Bx7Fnb275R_z24,5411
103
103
  pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
104
- pages/tests.py,sha256=-4EAtsfW3rmAVOCHaq6X_2rqLj0QEXkvJ5Lr3fY3QRw,125124
104
+ pages/tests.py,sha256=i1HLp8rNm63WuqFf9YgAEGWYowFC7SOyyg_7j17_buQ,126102
105
105
  pages/urls.py,sha256=Ne6yYJxgUAMieDpppJ149E-yh-oVi92fARiRPe-n4-s,1166
106
106
  pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
107
107
  pages/views.py,sha256=Ye7qGlO7IwkZO0oR1SzCpkEDTtGCJPmDJT-x6QQ8vaQ,45848
108
- arthexis-0.1.16.dist-info/METADATA,sha256=hQmESfUjXmKX-Sp0jIX99El92v5Q691UVjNobBUflWg,9998
109
- arthexis-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- arthexis-0.1.16.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
111
- arthexis-0.1.16.dist-info/RECORD,,
108
+ arthexis-0.1.17.dist-info/METADATA,sha256=rvPpFn4fwAZGm0JJ6Esu1r1A80uFC-tWCYhJOj0UNd8,9998
109
+ arthexis-0.1.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
+ arthexis-0.1.17.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
111
+ arthexis-0.1.17.dist-info/RECORD,,
config/settings.py CHANGED
@@ -480,6 +480,9 @@ AUTHENTICATION_BACKENDS = [
480
480
  "core.backends.RFIDBackend",
481
481
  ]
482
482
 
483
+ # Use the custom login view for all authentication redirects.
484
+ LOGIN_URL = "pages:login"
485
+
483
486
  # Issuer name used when generating otpauth URLs for authenticator apps.
484
487
  OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
485
488
 
core/admin.py CHANGED
@@ -1309,11 +1309,11 @@ class AssistantProfileInlineForm(ProfileFormMixin, forms.ModelForm):
1309
1309
  widget=forms.PasswordInput(render_value=True),
1310
1310
  help_text="Provide a plain key to create or rotate credentials.",
1311
1311
  )
1312
- profile_fields = ("user_key", "scopes", "is_active")
1312
+ profile_fields = ("assistant_name", "user_key", "scopes", "is_active")
1313
1313
 
1314
1314
  class Meta:
1315
1315
  model = AssistantProfile
1316
- fields = ("scopes", "is_active")
1316
+ fields = ("assistant_name", "scopes", "is_active")
1317
1317
 
1318
1318
  def __init__(self, *args, **kwargs):
1319
1319
  super().__init__(*args, **kwargs)
@@ -1475,7 +1475,7 @@ PROFILE_INLINE_CONFIG = {
1475
1475
  },
1476
1476
  AssistantProfile: {
1477
1477
  "form": AssistantProfileInlineForm,
1478
- "fields": ("user_key", "scopes", "is_active"),
1478
+ "fields": ("assistant_name", "user_key", "scopes", "is_active"),
1479
1479
  "readonly_fields": ("user_key_hash", "created_at", "last_used_at"),
1480
1480
  "template": "admin/edit_inline/profile_stacked.html",
1481
1481
  },
@@ -2016,7 +2016,7 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
2016
2016
  class AssistantProfileAdmin(
2017
2017
  ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
2018
2018
  ):
2019
- list_display = ("owner", "created_at", "last_used_at", "is_active")
2019
+ list_display = ("assistant_name", "owner", "created_at", "last_used_at", "is_active")
2020
2020
  readonly_fields = ("user_key_hash", "created_at", "last_used_at")
2021
2021
 
2022
2022
  change_form_template = "admin/workgroupassistantprofile_change_form.html"
@@ -2028,7 +2028,15 @@ class AssistantProfileAdmin(
2028
2028
  ("Credentials", {"fields": ("user_key_hash",)}),
2029
2029
  (
2030
2030
  "Configuration",
2031
- {"fields": ("scopes", "is_active", "created_at", "last_used_at")},
2031
+ {
2032
+ "fields": (
2033
+ "assistant_name",
2034
+ "scopes",
2035
+ "is_active",
2036
+ "created_at",
2037
+ "last_used_at",
2038
+ )
2039
+ },
2032
2040
  ),
2033
2041
  )
2034
2042
 
@@ -2835,6 +2843,7 @@ class RFIDResource(resources.ModelResource):
2835
2843
  "post_auth_command",
2836
2844
  "allowed",
2837
2845
  "color",
2846
+ "endianness",
2838
2847
  "kind",
2839
2848
  "released",
2840
2849
  "last_seen_on",
@@ -2849,6 +2858,7 @@ class RFIDResource(resources.ModelResource):
2849
2858
  "post_auth_command",
2850
2859
  "allowed",
2851
2860
  "color",
2861
+ "endianness",
2852
2862
  "kind",
2853
2863
  "released",
2854
2864
  "last_seen_on",
@@ -2919,11 +2929,12 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2919
2929
  "user_data_flag",
2920
2930
  "color",
2921
2931
  "kind",
2932
+ "endianness",
2922
2933
  "released",
2923
2934
  "allowed",
2924
2935
  "last_seen_on",
2925
2936
  )
2926
- list_filter = ("color", "released", "allowed")
2937
+ list_filter = ("color", "endianness", "released", "allowed")
2927
2938
  search_fields = ("label_id", "rfid", "custom_label")
2928
2939
  autocomplete_fields = ["energy_accounts"]
2929
2940
  raw_id_fields = ["reference"]
@@ -2933,6 +2944,8 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
2933
2944
  "print_release_form",
2934
2945
  "copy_rfids",
2935
2946
  "toggle_selected_user_data",
2947
+ "toggle_selected_released",
2948
+ "toggle_selected_allowed",
2936
2949
  ]
2937
2950
  readonly_fields = ("added_on", "last_seen_on")
2938
2951
  form = RFIDForm
@@ -3030,6 +3043,50 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3030
3043
  level=messages.WARNING,
3031
3044
  )
3032
3045
 
3046
+ @admin.action(description=_("Toggle Released flag"))
3047
+ def toggle_selected_released(self, request, queryset):
3048
+ manager = getattr(self.model, "all_objects", self.model.objects)
3049
+ toggled = 0
3050
+ for tag in queryset:
3051
+ new_state = not tag.released
3052
+ manager.filter(pk=tag.pk).update(released=new_state)
3053
+ tag.released = new_state
3054
+ toggled += 1
3055
+
3056
+ if toggled:
3057
+ self.message_user(
3058
+ request,
3059
+ ngettext(
3060
+ "Toggled released flag for %(count)d RFID.",
3061
+ "Toggled released flag for %(count)d RFIDs.",
3062
+ toggled,
3063
+ )
3064
+ % {"count": toggled},
3065
+ level=messages.SUCCESS,
3066
+ )
3067
+
3068
+ @admin.action(description=_("Toggle Allowed flag"))
3069
+ def toggle_selected_allowed(self, request, queryset):
3070
+ manager = getattr(self.model, "all_objects", self.model.objects)
3071
+ toggled = 0
3072
+ for tag in queryset:
3073
+ new_state = not tag.allowed
3074
+ manager.filter(pk=tag.pk).update(allowed=new_state)
3075
+ tag.allowed = new_state
3076
+ toggled += 1
3077
+
3078
+ if toggled:
3079
+ self.message_user(
3080
+ request,
3081
+ ngettext(
3082
+ "Toggled allowed flag for %(count)d RFID.",
3083
+ "Toggled allowed flag for %(count)d RFIDs.",
3084
+ toggled,
3085
+ )
3086
+ % {"count": toggled},
3087
+ level=messages.SUCCESS,
3088
+ )
3089
+
3033
3090
  @admin.action(description=_("Copy RFID"))
3034
3091
  def copy_rfids(self, request, queryset):
3035
3092
  if queryset.count() != 1:
@@ -3497,6 +3554,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3497
3554
  context["title"] = _("Scan RFIDs")
3498
3555
  context["opts"] = self.model._meta
3499
3556
  context["show_release_info"] = True
3557
+ context["default_endianness"] = RFID.BIG_ENDIAN
3500
3558
  return render(request, "admin/core/rfid/scan.html", context)
3501
3559
 
3502
3560
  def scan_next(self, request):
@@ -3510,9 +3568,11 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
3510
3568
  return JsonResponse({"error": "Invalid JSON payload"}, status=400)
3511
3569
  rfid = payload.get("rfid") or payload.get("value")
3512
3570
  kind = payload.get("kind")
3513
- result = validate_rfid_value(rfid, kind=kind)
3571
+ endianness = payload.get("endianness")
3572
+ result = validate_rfid_value(rfid, kind=kind, endianness=endianness)
3514
3573
  else:
3515
- result = scan_sources(request)
3574
+ endianness = request.GET.get("endianness")
3575
+ result = scan_sources(request, endianness=endianness)
3516
3576
  status = 500 if result.get("error") else 200
3517
3577
  return JsonResponse(result, status=status)
3518
3578
 
core/backends.py CHANGED
@@ -90,6 +90,7 @@ class RFIDBackend:
90
90
  env = os.environ.copy()
91
91
  env["RFID_VALUE"] = rfid_value
92
92
  env["RFID_LABEL_ID"] = str(tag.pk)
93
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
93
94
  try:
94
95
  completed = subprocess.run(
95
96
  command,
@@ -117,6 +118,7 @@ class RFIDBackend:
117
118
  env = os.environ.copy()
118
119
  env["RFID_VALUE"] = rfid_value
119
120
  env["RFID_LABEL_ID"] = str(tag.pk)
121
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
120
122
  with contextlib.suppress(Exception):
121
123
  subprocess.Popen(
122
124
  post_command,
core/changelog.py CHANGED
@@ -154,9 +154,53 @@ def _parse_sections(text: str) -> List[ChangelogSection]:
154
154
  return sections
155
155
 
156
156
 
157
+ def _latest_release_version(previous_text: str) -> Optional[str]:
158
+ for section in _parse_sections(previous_text):
159
+ if section.version:
160
+ return section.version
161
+ return None
162
+
163
+
164
+ def _find_release_commit(version: str) -> Optional[str]:
165
+ normalized = version.lstrip("v")
166
+ search_terms = [
167
+ f"Release v{normalized}",
168
+ f"Release {normalized}",
169
+ f"pre-release commit v{normalized}",
170
+ f"pre-release commit {normalized}",
171
+ ]
172
+ for term in search_terms:
173
+ proc = subprocess.run(
174
+ [
175
+ "git",
176
+ "log",
177
+ "--max-count=1",
178
+ "--format=%H",
179
+ "--fixed-strings",
180
+ f"--grep={term}",
181
+ ],
182
+ capture_output=True,
183
+ text=True,
184
+ check=False,
185
+ )
186
+ sha = proc.stdout.strip()
187
+ if sha:
188
+ return sha.splitlines()[0]
189
+ return None
190
+
191
+
192
+ def _resolve_release_commit_from_text(previous_text: str) -> Optional[str]:
193
+ version = _latest_release_version(previous_text)
194
+ if not version:
195
+ return None
196
+ return _find_release_commit(version)
197
+
198
+
157
199
  def _merge_sections(
158
200
  new_sections: Iterable[ChangelogSection],
159
201
  old_sections: Iterable[ChangelogSection],
202
+ *,
203
+ reopen_latest: bool = False,
160
204
  ) -> List[ChangelogSection]:
161
205
  merged = list(new_sections)
162
206
  old_sections_list = list(old_sections)
@@ -199,7 +243,8 @@ def _merge_sections(
199
243
  existing = version_to_section.get(old.version)
200
244
  if existing is None:
201
245
  if (
202
- first_release_version
246
+ reopen_latest
247
+ and first_release_version
203
248
  and old.version == first_release_version
204
249
  and not reopened_latest_version
205
250
  and unreleased_section is not None
@@ -274,29 +319,45 @@ def _resolve_start_tag(explicit: str | None = None) -> Optional[str]:
274
319
  return None
275
320
 
276
321
 
277
- def determine_range_spec(start_tag: str | None = None) -> str:
322
+ def determine_range_spec(
323
+ start_tag: str | None = None, *, previous_text: str | None = None
324
+ ) -> str:
278
325
  """Return the git range specification to build the changelog."""
279
326
 
280
327
  resolved = _resolve_start_tag(start_tag)
281
328
  if resolved:
282
329
  return f"{resolved}..HEAD"
330
+
331
+ if previous_text:
332
+ release_commit = _resolve_release_commit_from_text(previous_text)
333
+ if release_commit:
334
+ return f"{release_commit}..HEAD"
335
+
283
336
  return "HEAD"
284
337
 
285
338
 
286
339
  def collect_sections(
287
- *, range_spec: str = "HEAD", previous_text: str | None = None
340
+ *,
341
+ range_spec: str = "HEAD",
342
+ previous_text: str | None = None,
343
+ reopen_latest: bool = False,
288
344
  ) -> List[ChangelogSection]:
289
345
  """Return changelog sections for *range_spec*.
290
346
 
291
347
  When ``previous_text`` is provided, sections not regenerated in the current run
292
- are appended so long as they can be parsed from the existing changelog.
348
+ are appended so long as they can be parsed from the existing changelog. Set
349
+ ``reopen_latest`` to ``True`` when the caller intends to move the most recent
350
+ release notes back into the ``Unreleased`` section (for example, when
351
+ preparing a release retry before a new tag is created).
293
352
  """
294
353
 
295
354
  commits = _read_commits(range_spec)
296
355
  sections = _sections_from_commits(commits)
297
356
  if previous_text:
298
357
  old_sections = _parse_sections(previous_text)
299
- sections = _merge_sections(sections, old_sections)
358
+ sections = _merge_sections(
359
+ sections, old_sections, reopen_latest=reopen_latest
360
+ )
300
361
  return sections
301
362
 
302
363
 
core/models.py CHANGED
@@ -1851,6 +1851,17 @@ class RFID(Entity):
1851
1851
  choices=KIND_CHOICES,
1852
1852
  default=CLASSIC,
1853
1853
  )
1854
+ BIG_ENDIAN = "BIG"
1855
+ LITTLE_ENDIAN = "LITTLE"
1856
+ ENDIANNESS_CHOICES = [
1857
+ (BIG_ENDIAN, _("Big endian")),
1858
+ (LITTLE_ENDIAN, _("Little endian")),
1859
+ ]
1860
+ endianness = models.CharField(
1861
+ max_length=6,
1862
+ choices=ENDIANNESS_CHOICES,
1863
+ default=BIG_ENDIAN,
1864
+ )
1854
1865
  reference = models.ForeignKey(
1855
1866
  "Reference",
1856
1867
  null=True,
@@ -1902,6 +1913,8 @@ class RFID(Entity):
1902
1913
  self.key_b = self.key_b.upper()
1903
1914
  if self.kind:
1904
1915
  self.kind = self.kind.upper()
1916
+ if self.endianness:
1917
+ self.endianness = self.normalize_endianness(self.endianness)
1905
1918
  super().save(*args, **kwargs)
1906
1919
  if not self.allowed:
1907
1920
  self.energy_accounts.clear()
@@ -1909,6 +1922,17 @@ class RFID(Entity):
1909
1922
  def __str__(self): # pragma: no cover - simple representation
1910
1923
  return str(self.label_id)
1911
1924
 
1925
+ @classmethod
1926
+ def normalize_endianness(cls, value: object) -> str:
1927
+ """Return a valid endianness value, defaulting to BIG."""
1928
+
1929
+ if isinstance(value, str):
1930
+ candidate = value.strip().upper()
1931
+ valid = {choice[0] for choice in cls.ENDIANNESS_CHOICES}
1932
+ if candidate in valid:
1933
+ return candidate
1934
+ return cls.BIG_ENDIAN
1935
+
1912
1936
  @classmethod
1913
1937
  def next_scan_label(
1914
1938
  cls, *, step: int | None = None, start: int | None = None
@@ -1971,13 +1995,39 @@ class RFID(Entity):
1971
1995
 
1972
1996
  @classmethod
1973
1997
  def register_scan(
1974
- cls, rfid: str, *, kind: str | None = None
1998
+ cls,
1999
+ rfid: str,
2000
+ *,
2001
+ kind: str | None = None,
2002
+ endianness: str | None = None,
1975
2003
  ) -> tuple["RFID", bool]:
1976
2004
  """Return or create an RFID that was detected via scanning."""
1977
2005
 
1978
- normalized = (rfid or "").upper()
1979
- existing = cls.objects.filter(rfid=normalized).first()
2006
+ normalized = "".join((rfid or "").split()).upper()
2007
+ desired_endianness = cls.normalize_endianness(endianness)
2008
+ alternate = None
2009
+ if normalized and len(normalized) % 2 == 0:
2010
+ bytes_list = [normalized[i : i + 2] for i in range(0, len(normalized), 2)]
2011
+ bytes_list.reverse()
2012
+ alternate_candidate = "".join(bytes_list)
2013
+ if alternate_candidate != normalized:
2014
+ alternate = alternate_candidate
2015
+
2016
+ existing = None
2017
+ if normalized:
2018
+ existing = cls.objects.filter(rfid=normalized).first()
2019
+ if not existing and alternate:
2020
+ existing = cls.objects.filter(rfid=alternate).first()
1980
2021
  if existing:
2022
+ update_fields: list[str] = []
2023
+ if normalized and existing.rfid != normalized:
2024
+ existing.rfid = normalized
2025
+ update_fields.append("rfid")
2026
+ if existing.endianness != desired_endianness:
2027
+ existing.endianness = desired_endianness
2028
+ update_fields.append("endianness")
2029
+ if update_fields:
2030
+ existing.save(update_fields=update_fields)
1981
2031
  return existing, False
1982
2032
 
1983
2033
  attempts = 0
@@ -1990,6 +2040,7 @@ class RFID(Entity):
1990
2040
  "rfid": normalized,
1991
2041
  "allowed": True,
1992
2042
  "released": False,
2043
+ "endianness": desired_endianness,
1993
2044
  }
1994
2045
  if kind:
1995
2046
  create_kwargs["kind"] = kind
@@ -3539,7 +3590,8 @@ class AssistantProfile(Profile):
3539
3590
  """
3540
3591
 
3541
3592
  id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
3542
- profile_fields = ("user_key_hash", "scopes", "is_active")
3593
+ profile_fields = ("assistant_name", "user_key_hash", "scopes", "is_active")
3594
+ assistant_name = models.CharField(max_length=100, default="Assistant")
3543
3595
  user_key_hash = models.CharField(max_length=64, unique=True)
3544
3596
  scopes = models.JSONField(default=list, blank=True)
3545
3597
  created_at = models.DateTimeField(auto_now_add=True)
@@ -3586,8 +3638,7 @@ class AssistantProfile(Profile):
3586
3638
  self.save(update_fields=["last_used_at"])
3587
3639
 
3588
3640
  def __str__(self) -> str: # pragma: no cover - simple representation
3589
- owner = self.owner_display()
3590
- return f"AssistantProfile for {owner}" if owner else "AssistantProfile"
3641
+ return self.assistant_name or "AssistantProfile"
3591
3642
 
3592
3643
 
3593
3644
  def validate_relative_url(value: str) -> None:
core/release.py CHANGED
@@ -114,6 +114,21 @@ class ReleaseError(Exception):
114
114
  pass
115
115
 
116
116
 
117
+ class PostPublishWarning(ReleaseError):
118
+ """Raised when distribution uploads succeed but post-publish tasks need attention."""
119
+
120
+ def __init__(
121
+ self,
122
+ message: str,
123
+ *,
124
+ uploaded: Sequence[str],
125
+ followups: Optional[Sequence[str]] = None,
126
+ ) -> None:
127
+ super().__init__(message)
128
+ self.uploaded = list(uploaded)
129
+ self.followups = list(followups or [])
130
+
131
+
117
132
  class TestsFailed(ReleaseError):
118
133
  """Raised when the test suite fails.
119
134
 
@@ -711,8 +726,46 @@ def publish(
711
726
  uploaded.append(target.name)
712
727
 
713
728
  tag_name = f"v{version}"
714
- _run(["git", "tag", tag_name])
715
- _push_tag(tag_name, package)
729
+ try:
730
+ _run(["git", "tag", tag_name])
731
+ except subprocess.CalledProcessError as exc:
732
+ details = _format_subprocess_error(exc)
733
+ if uploaded:
734
+ uploads = ", ".join(uploaded)
735
+ if details:
736
+ message = (
737
+ f"Upload to {uploads} completed, but creating git tag {tag_name} failed: {details}"
738
+ )
739
+ else:
740
+ message = (
741
+ f"Upload to {uploads} completed, but creating git tag {tag_name} failed."
742
+ )
743
+ followups = [f"Create and push git tag {tag_name} manually once the repository is ready."]
744
+ raise PostPublishWarning(
745
+ message,
746
+ uploaded=uploaded,
747
+ followups=followups,
748
+ ) from exc
749
+ raise ReleaseError(
750
+ f"Failed to create git tag {tag_name}: {details or exc}"
751
+ ) from exc
752
+
753
+ try:
754
+ _push_tag(tag_name, package)
755
+ except ReleaseError as exc:
756
+ if uploaded:
757
+ uploads = ", ".join(uploaded)
758
+ message = f"Upload to {uploads} completed, but {exc}"
759
+ followups = [
760
+ f"Push git tag {tag_name} to origin after resolving the reported issue."
761
+ ]
762
+ warning = PostPublishWarning(
763
+ message,
764
+ uploaded=uploaded,
765
+ followups=followups,
766
+ )
767
+ raise warning from exc
768
+ raise
716
769
  return uploaded
717
770
 
718
771
 
core/system.py CHANGED
@@ -171,7 +171,7 @@ def _regenerate_changelog() -> None:
171
171
  previous_text = (
172
172
  changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
173
173
  )
174
- range_spec = changelog_utils.determine_range_spec()
174
+ range_spec = changelog_utils.determine_range_spec(previous_text=previous_text)
175
175
  sections = changelog_utils.collect_sections(
176
176
  range_spec=range_spec, previous_text=previous_text
177
177
  )
core/tasks.py CHANGED
@@ -230,12 +230,6 @@ def check_github_updates() -> None:
230
230
 
231
231
  subprocess.run(args, cwd=base_dir, check=True)
232
232
 
233
- if shutil.which("gway"):
234
- try:
235
- subprocess.run(["gway", "upgrade"], check=True)
236
- except subprocess.CalledProcessError:
237
- logger.warning("gway upgrade failed; continuing anyway", exc_info=True)
238
-
239
233
  service_file = base_dir / "locks/service.lck"
240
234
  if service_file.exists():
241
235
  service = service_file.read_text().strip()