machine_access_control 0.2.3__tar.gz → 0.3.0__tar.gz
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.
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/PKG-INFO +8 -6
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/pyproject.toml +7 -7
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/models/machine.py +109 -15
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/LICENSE +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/README.rst +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/cli_utils.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/models/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/models/users.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/neongetter.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/py.typed +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/slack_handler.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/utils.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/views/__init__.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/views/api.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/views/machine.py +0 -0
- {machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/views/prometheus.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: machine_access_control
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Decatur Makers Machine Access Control package
|
|
5
|
-
Home-page: https://github.com/jantman/machine_access_control
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Author: Jason Antman
|
|
8
8
|
Author-email: jason@jasonantman.com
|
|
9
9
|
Requires-Python: >=3.12,<4.0
|
|
@@ -12,18 +12,20 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Requires-Dist: aiohttp (>=3.12.14,<4.0.0)
|
|
16
17
|
Requires-Dist: asyncio (>=3.4.3,<4.0.0)
|
|
17
|
-
Requires-Dist: filelock (>=3.
|
|
18
|
+
Requires-Dist: filelock (>=3.16.1,<4.0.0)
|
|
18
19
|
Requires-Dist: humanize (>=4.11.0,<5.0.0)
|
|
19
20
|
Requires-Dist: jsonschema (>=4.23.0,<5.0.0)
|
|
20
21
|
Requires-Dist: prometheus-client (>=0.20.0,<0.21.0)
|
|
21
22
|
Requires-Dist: quart (>=0.19.8,<0.20.0)
|
|
22
|
-
Requires-Dist: requests (>=2.32.
|
|
23
|
+
Requires-Dist: requests (>=2.32.4,<3.0.0)
|
|
23
24
|
Requires-Dist: slack-bolt (>=1.21.3,<2.0.0)
|
|
24
25
|
Requires-Dist: slack-sdk (>=3.33.5,<4.0.0)
|
|
25
26
|
Project-URL: Changelog, https://github.com/jantman/machine_access_control/releases
|
|
26
27
|
Project-URL: Documentation, https://github.com/jantman/machine_access_control
|
|
28
|
+
Project-URL: Homepage, https://github.com/jantman/machine_access_control
|
|
27
29
|
Project-URL: Repository, https://github.com/jantman/machine_access_control
|
|
28
30
|
Description-Content-Type: text/x-rst
|
|
29
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "machine_access_control"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Decatur Makers Machine Access Control package"
|
|
5
5
|
authors = ["Jason Antman <jason@jasonantman.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -25,13 +25,13 @@ Changelog = "https://github.com/jantman/machine_access_control/releases"
|
|
|
25
25
|
[tool.poetry.dependencies]
|
|
26
26
|
python = "^3.12"
|
|
27
27
|
jsonschema = "^4.23.0"
|
|
28
|
-
requests = "^2.32.
|
|
29
|
-
filelock = "^3.
|
|
28
|
+
requests = "^2.32.4"
|
|
29
|
+
filelock = "^3.16.1"
|
|
30
30
|
prometheus-client = "^0.20.0"
|
|
31
31
|
quart = "^0.19.8"
|
|
32
32
|
asyncio = "^3.4.3"
|
|
33
33
|
slack-bolt = "^1.21.3"
|
|
34
|
-
aiohttp = "^3.
|
|
34
|
+
aiohttp = "^3.12.14"
|
|
35
35
|
slack-sdk = "^3.33.5"
|
|
36
36
|
humanize = "^4.11.0"
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ Pygments = ">=2.10.0"
|
|
|
40
40
|
black = ">=21.10b0"
|
|
41
41
|
coverage = {extras = ["toml"], version = ">=6.2"}
|
|
42
42
|
darglint = ">=1.8.1"
|
|
43
|
-
flake8 = ">=
|
|
43
|
+
flake8 = ">=7.1.1"
|
|
44
44
|
flake8-bandit = ">=2.1.2"
|
|
45
45
|
flake8-bugbear = ">=21.9.2"
|
|
46
46
|
flake8-docstrings = ">=1.6.0"
|
|
@@ -49,7 +49,7 @@ furo = ">=2021.11.12"
|
|
|
49
49
|
isort = ">=5.10.1"
|
|
50
50
|
mypy = ">=0.930"
|
|
51
51
|
nox = ">=2024.4.15"
|
|
52
|
-
nox-poetry = ">= 1.0
|
|
52
|
+
nox-poetry = ">= 1.2.0"
|
|
53
53
|
pep8-naming = ">=0.12.1"
|
|
54
54
|
pre-commit = ">=2.16.0"
|
|
55
55
|
pre-commit-hooks = ">=4.1.0"
|
|
@@ -65,7 +65,7 @@ responses = "^0.25.3"
|
|
|
65
65
|
types-requests = "^2.32.0.20240712"
|
|
66
66
|
faker = "^26.3.0"
|
|
67
67
|
pytest-html = "^4.1.1"
|
|
68
|
-
poetry-plugin-export = "^1.
|
|
68
|
+
poetry-plugin-export = "^1.9.0"
|
|
69
69
|
freezegun = "^1.5.1"
|
|
70
70
|
|
|
71
71
|
[tool.isort]
|
|
@@ -53,6 +53,13 @@ CONFIG_SCHEMA: Dict[str, Any] = {
|
|
|
53
53
|
"but log and display a warning if the "
|
|
54
54
|
"operator is not authorized.",
|
|
55
55
|
},
|
|
56
|
+
"always_enabled": {
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"description": "If set, machine is always enabled and "
|
|
59
|
+
"does not require RFID authentication. "
|
|
60
|
+
"Displays 'Always On' and relay is always "
|
|
61
|
+
"on unless Oopsed or Locked.",
|
|
62
|
+
},
|
|
56
63
|
},
|
|
57
64
|
"additionalProperties": False,
|
|
58
65
|
"description": "Unique machine name, alphanumeric _ and - only.",
|
|
@@ -69,6 +76,7 @@ class Machine:
|
|
|
69
76
|
name: str,
|
|
70
77
|
authorizations_or: List[str],
|
|
71
78
|
unauthorized_warn_only: bool = False,
|
|
79
|
+
always_enabled: bool = False,
|
|
72
80
|
):
|
|
73
81
|
"""Initialize a new MachineState instance."""
|
|
74
82
|
#: The name of the machine
|
|
@@ -78,6 +86,8 @@ class Machine:
|
|
|
78
86
|
#: Whether to allow anyone to operate machine regardless of
|
|
79
87
|
#: authorization, just logging/displaying a warning if unauthorized
|
|
80
88
|
self.unauthorized_warn_only: bool = unauthorized_warn_only
|
|
89
|
+
#: Whether machine is always enabled without RFID authentication
|
|
90
|
+
self.always_enabled: bool = always_enabled
|
|
81
91
|
#: state of the machine
|
|
82
92
|
self.state: "MachineState" = MachineState(self)
|
|
83
93
|
|
|
@@ -142,6 +152,7 @@ class Machine:
|
|
|
142
152
|
"name": self.name,
|
|
143
153
|
"authorizations_or": self.authorizations_or,
|
|
144
154
|
"unauthorized_warn_only": self.unauthorized_warn_only,
|
|
155
|
+
"always_enabled": self.always_enabled,
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
|
|
@@ -187,6 +198,8 @@ class MachineState:
|
|
|
187
198
|
|
|
188
199
|
LOCKOUT_DISPLAY_TEXT: str = "Down for\nmaintenance"
|
|
189
200
|
|
|
201
|
+
ALWAYS_ON_DISPLAY_TEXT: str = "Always On"
|
|
202
|
+
|
|
190
203
|
STATUS_LED_BRIGHTNESS: float = 0.5
|
|
191
204
|
|
|
192
205
|
def __init__(self, machine: Machine, load_state: bool = True):
|
|
@@ -290,18 +303,25 @@ class MachineState:
|
|
|
290
303
|
async def _handle_reboot(self) -> None:
|
|
291
304
|
"""Handle when the ESP32 (MCU) has rebooted since last checkin.
|
|
292
305
|
|
|
293
|
-
This logs out the current user if logged in and
|
|
294
|
-
|
|
306
|
+
This logs out the current user if logged in and resets the machine state.
|
|
307
|
+
For always-enabled machines, restores the always-on state.
|
|
295
308
|
"""
|
|
296
309
|
logging.getLogger("AUTH").warning(
|
|
297
310
|
"Machine %s rebooted; resetting relay and RFID state", self.machine.name
|
|
298
311
|
)
|
|
299
312
|
# locking handled in update()
|
|
300
|
-
self.relay_desired_state = False
|
|
301
313
|
self.current_user = None
|
|
302
|
-
|
|
303
|
-
self.
|
|
304
|
-
|
|
314
|
+
# Restore always-enabled state if applicable
|
|
315
|
+
if self.machine.always_enabled:
|
|
316
|
+
self.relay_desired_state = True
|
|
317
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
318
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
319
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
320
|
+
else:
|
|
321
|
+
self.relay_desired_state = False
|
|
322
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
323
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
324
|
+
self.status_led_brightness = 0.0
|
|
305
325
|
# log to Slack, if enabled
|
|
306
326
|
slack: Optional["SlackHandler"] = current_app.config.get("SLACK_HANDLER")
|
|
307
327
|
if not slack:
|
|
@@ -329,11 +349,18 @@ class MachineState:
|
|
|
329
349
|
)
|
|
330
350
|
with self._lock:
|
|
331
351
|
self.is_locked_out = False
|
|
332
|
-
self.relay_desired_state = False
|
|
333
352
|
self.current_user = None
|
|
334
|
-
|
|
335
|
-
self.
|
|
336
|
-
|
|
353
|
+
# Restore always-enabled state if applicable
|
|
354
|
+
if self.machine.always_enabled:
|
|
355
|
+
self.relay_desired_state = True
|
|
356
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
357
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
358
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
359
|
+
else:
|
|
360
|
+
self.relay_desired_state = False
|
|
361
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
362
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
363
|
+
self.status_led_brightness = 0.0
|
|
337
364
|
|
|
338
365
|
def oops(self, do_locking: bool = True) -> None:
|
|
339
366
|
"""Oops the machine."""
|
|
@@ -355,11 +382,18 @@ class MachineState:
|
|
|
355
382
|
locker = self._lock if do_locking else nullcontext()
|
|
356
383
|
with locker:
|
|
357
384
|
self.is_oopsed = False
|
|
358
|
-
self.relay_desired_state = False
|
|
359
385
|
self.current_user = None
|
|
360
|
-
|
|
361
|
-
self.
|
|
362
|
-
|
|
386
|
+
# Restore always-enabled state if applicable
|
|
387
|
+
if self.machine.always_enabled:
|
|
388
|
+
self.relay_desired_state = True
|
|
389
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
390
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
391
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
392
|
+
else:
|
|
393
|
+
self.relay_desired_state = False
|
|
394
|
+
self.display_text = self.DEFAULT_DISPLAY_TEXT
|
|
395
|
+
self.status_led_rgb = (0.0, 0.0, 0.0)
|
|
396
|
+
self.status_led_brightness = 0
|
|
363
397
|
|
|
364
398
|
async def update(
|
|
365
399
|
self,
|
|
@@ -398,7 +432,21 @@ class MachineState:
|
|
|
398
432
|
if oops:
|
|
399
433
|
await self._handle_oops(users)
|
|
400
434
|
self.last_update = time()
|
|
401
|
-
|
|
435
|
+
# Handle always-enabled machines - track RFID but maintain always-on state
|
|
436
|
+
if (
|
|
437
|
+
self.machine.always_enabled
|
|
438
|
+
and not self.is_oopsed
|
|
439
|
+
and not self.is_locked_out
|
|
440
|
+
):
|
|
441
|
+
self.relay_desired_state = True
|
|
442
|
+
self.display_text = self.ALWAYS_ON_DISPLAY_TEXT
|
|
443
|
+
self.status_led_rgb = (0.0, 1.0, 0.0)
|
|
444
|
+
self.status_led_brightness = self.STATUS_LED_BRIGHTNESS
|
|
445
|
+
self.last_update = time()
|
|
446
|
+
# Track RFID changes for logging/auditing purposes
|
|
447
|
+
if rfid_value != self.rfid_value:
|
|
448
|
+
await self._handle_rfid_tracking_always_enabled(users, rfid_value)
|
|
449
|
+
elif rfid_value != self.rfid_value:
|
|
402
450
|
if rfid_value is None:
|
|
403
451
|
await self._handle_rfid_remove()
|
|
404
452
|
else:
|
|
@@ -553,6 +601,52 @@ class MachineState:
|
|
|
553
601
|
f"UNAUTHORIZED user {user.full_name}"
|
|
554
602
|
)
|
|
555
603
|
|
|
604
|
+
async def _handle_rfid_tracking_always_enabled(
|
|
605
|
+
self, users: UsersConfig, rfid_value: Optional[str]
|
|
606
|
+
) -> None:
|
|
607
|
+
"""Track RFID changes for always-enabled machines without changing state.
|
|
608
|
+
|
|
609
|
+
This method logs RFID insertions and removals for auditing purposes while
|
|
610
|
+
maintaining the always-on state of the machine.
|
|
611
|
+
"""
|
|
612
|
+
# locking handled in update()
|
|
613
|
+
if rfid_value is None:
|
|
614
|
+
# RFID removed
|
|
615
|
+
logging.getLogger("AUTH").info(
|
|
616
|
+
"RFID removed on always-enabled machine %s (was %s); session duration %d seconds",
|
|
617
|
+
self.machine.name,
|
|
618
|
+
self.current_user.full_name if self.current_user else self.rfid_value,
|
|
619
|
+
(
|
|
620
|
+
time() - cast(float, self.rfid_present_since)
|
|
621
|
+
if self.rfid_present_since
|
|
622
|
+
else 0
|
|
623
|
+
),
|
|
624
|
+
)
|
|
625
|
+
self.rfid_value = None
|
|
626
|
+
self.rfid_present_since = None
|
|
627
|
+
self.current_user = None
|
|
628
|
+
# State remains always-on (relay/display/LED not changed)
|
|
629
|
+
else:
|
|
630
|
+
# RFID inserted
|
|
631
|
+
self.rfid_present_since = time()
|
|
632
|
+
self.rfid_value = rfid_value
|
|
633
|
+
user: Optional[User] = users.users_by_fob.get(rfid_value)
|
|
634
|
+
if user:
|
|
635
|
+
self.current_user = user
|
|
636
|
+
logging.getLogger("AUTH").info(
|
|
637
|
+
"RFID inserted on always-enabled machine %s by %s (%s)",
|
|
638
|
+
self.machine.name,
|
|
639
|
+
user.full_name,
|
|
640
|
+
rfid_value,
|
|
641
|
+
)
|
|
642
|
+
else:
|
|
643
|
+
logging.getLogger("AUTH").warning(
|
|
644
|
+
"RFID inserted on always-enabled machine %s by unknown fob %s",
|
|
645
|
+
self.machine.name,
|
|
646
|
+
rfid_value,
|
|
647
|
+
)
|
|
648
|
+
# State remains always-on (relay/display/LED not changed)
|
|
649
|
+
|
|
556
650
|
async def _user_is_authorized(
|
|
557
651
|
self, user: User, slack: Optional["SlackHandler"] = None
|
|
558
652
|
) -> bool:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{machine_access_control-0.2.3 → machine_access_control-0.3.0}/src/dm_mac/views/prometheus.py
RENAMED
|
File without changes
|