pyzkaccess 1.1__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.
- pyzkaccess/__init__.py +16 -0
- pyzkaccess/_setup.py +231 -0
- pyzkaccess/aux_input.py +76 -0
- pyzkaccess/cli.py +1300 -0
- pyzkaccess/common.py +425 -0
- pyzkaccess/ctypes_.py +42 -0
- pyzkaccess/device.py +235 -0
- pyzkaccess/device_data/__init__.py +0 -0
- pyzkaccess/device_data/model.py +347 -0
- pyzkaccess/device_data/queryset.py +397 -0
- pyzkaccess/door.py +142 -0
- pyzkaccess/enums.py +672 -0
- pyzkaccess/event.py +335 -0
- pyzkaccess/exceptions.py +30 -0
- pyzkaccess/main.py +430 -0
- pyzkaccess/param.py +414 -0
- pyzkaccess/reader.py +109 -0
- pyzkaccess/relay.py +135 -0
- pyzkaccess/sdk.py +456 -0
- pyzkaccess/tables.py +188 -0
- pyzkaccess-1.1.dist-info/LICENSE +201 -0
- pyzkaccess-1.1.dist-info/METADATA +151 -0
- pyzkaccess-1.1.dist-info/RECORD +25 -0
- pyzkaccess-1.1.dist-info/WHEEL +4 -0
- pyzkaccess-1.1.dist-info/entry_points.txt +3 -0
pyzkaccess/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# flake8: noqa
|
|
2
|
+
from .aux_input import *
|
|
3
|
+
from .common import *
|
|
4
|
+
from .ctypes_ import *
|
|
5
|
+
from .device import *
|
|
6
|
+
from .device_data import *
|
|
7
|
+
from .door import *
|
|
8
|
+
from .enums import *
|
|
9
|
+
from .event import *
|
|
10
|
+
from .exceptions import *
|
|
11
|
+
from .main import *
|
|
12
|
+
from .param import *
|
|
13
|
+
from .reader import *
|
|
14
|
+
from .relay import *
|
|
15
|
+
from .sdk import *
|
|
16
|
+
from .tables import *
|
pyzkaccess/_setup.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
__all__ = ["setup"]
|
|
2
|
+
import contextlib
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import urllib.request
|
|
11
|
+
import zipfile
|
|
12
|
+
from ctypes import POINTER, c_char_p, c_int, c_ulong, c_void_p
|
|
13
|
+
from ctypes.wintypes import BOOL, DWORD, HANDLE, HINSTANCE, HKEY, HWND
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Final, Iterable, Iterator, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from pyzkaccess.ctypes_ import windll
|
|
18
|
+
|
|
19
|
+
PULL_SDK_FETCH_URL: Final[str] = "https://server.zkteco.eu/ddfb/pull_sdk.zip"
|
|
20
|
+
PULL_SDK_SUBDIR_GLOB: Final[str] = "SDK*"
|
|
21
|
+
PULL_SDK_DLLS: Final[Tuple[str, ...]] = (
|
|
22
|
+
"plcommpro.dll",
|
|
23
|
+
"plcomms.dll",
|
|
24
|
+
"plrscagent.dll",
|
|
25
|
+
"plrscomm.dll",
|
|
26
|
+
"pltcpcomm.dll",
|
|
27
|
+
"plusbcomm.dll",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StepFailedError(Exception):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextlib.contextmanager
|
|
36
|
+
def step(title: str, simple: bool = True) -> Iterator[None]:
|
|
37
|
+
sys.stdout.write(f"> {title}" + (": " if simple else "\n"))
|
|
38
|
+
sys.stdout.flush()
|
|
39
|
+
try:
|
|
40
|
+
yield
|
|
41
|
+
if simple:
|
|
42
|
+
sys.stdout.write("OK\n")
|
|
43
|
+
except StepFailedError as e:
|
|
44
|
+
if simple:
|
|
45
|
+
sys.stdout.write("ERROR\n")
|
|
46
|
+
sys.stderr.write(f"\nERROR: {e}\n")
|
|
47
|
+
sys.exit(3)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup(interactive: bool, path: Optional[str]) -> None:
|
|
51
|
+
with step("Operating system"):
|
|
52
|
+
if sys.platform != "win32":
|
|
53
|
+
raise StepFailedError(
|
|
54
|
+
f"OS '{sys.platform}' is not supported\n"
|
|
55
|
+
f"Only Windows/Wine 32-bit platform is supported (this is a limitation of PULL SDK)\n"
|
|
56
|
+
f"See the docs https://bdragon300.github.io/pyzkaccess/#installation for more information\n"
|
|
57
|
+
)
|
|
58
|
+
sys.stdout.write("[win32] ")
|
|
59
|
+
|
|
60
|
+
with step("Python version"):
|
|
61
|
+
if sys.maxsize.bit_length() > 32:
|
|
62
|
+
raise StepFailedError(
|
|
63
|
+
f"Python version must be 32-bit, but {sys.maxsize.bit_length() + 1} bit version installed\n"
|
|
64
|
+
f"32-bit Python is available to download at https://www.python.org/downloads/windows/\n"
|
|
65
|
+
f"See the docs https://bdragon300.github.io/pyzkaccess/#installation for more information\n"
|
|
66
|
+
)
|
|
67
|
+
sys.stdout.write("[32-bit] ")
|
|
68
|
+
|
|
69
|
+
with step("System root"):
|
|
70
|
+
system_root = os.environ.get("SystemRoot")
|
|
71
|
+
if system_root is None:
|
|
72
|
+
raise StepFailedError(
|
|
73
|
+
"SystemRoot environment variable is not set\n"
|
|
74
|
+
"This variable must be present on Windows platform, please check OS settings\n"
|
|
75
|
+
)
|
|
76
|
+
sys.stdout.write(f"[{system_root}] ")
|
|
77
|
+
|
|
78
|
+
with step("Library root"):
|
|
79
|
+
is_win64 = platform.machine().endswith("64")
|
|
80
|
+
lib_root = Path(system_root) / ("SysWOW64" if is_win64 else "System32")
|
|
81
|
+
if not lib_root.exists():
|
|
82
|
+
raise StepFailedError(
|
|
83
|
+
f"Library root '{lib_root}' not found\n"
|
|
84
|
+
f"Please check the OS settings or contact the system administrator\n"
|
|
85
|
+
)
|
|
86
|
+
sys.stdout.write(f"[{lib_root}] ")
|
|
87
|
+
|
|
88
|
+
with step("Install ZKTeco PULL SDK", simple=False):
|
|
89
|
+
already_installed = all((lib_root / f).exists() for f in PULL_SDK_DLLS)
|
|
90
|
+
if not already_installed:
|
|
91
|
+
if interactive and path is None:
|
|
92
|
+
path = input(
|
|
93
|
+
f">> Enter HTTP URL, zip archive or directory with PULL SDK [default: {PULL_SDK_FETCH_URL}]: "
|
|
94
|
+
)
|
|
95
|
+
if not path:
|
|
96
|
+
path = PULL_SDK_FETCH_URL
|
|
97
|
+
|
|
98
|
+
sdk_contents_dir = _fetch_and_extract(path)
|
|
99
|
+
_install_dlls(sdk_contents_dir, lib_root, interactive)
|
|
100
|
+
else:
|
|
101
|
+
sys.stdout.write(">> PULL SDK already installed\n")
|
|
102
|
+
|
|
103
|
+
sys.stdout.write("Setup complete, everything looks good!\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _fetch_and_extract(any_path: str) -> Path:
|
|
107
|
+
if any_path.startswith("http"):
|
|
108
|
+
sys.stdout.write(f">> Downloading PULL SDK files from {any_path}...\n")
|
|
109
|
+
with contextlib.closing(urllib.request.urlopen(any_path)) as resp:
|
|
110
|
+
with tempfile.NamedTemporaryFile(delete=False) as fp:
|
|
111
|
+
fp.write(resp.read())
|
|
112
|
+
any_path = fp.name
|
|
113
|
+
|
|
114
|
+
fs_path = Path(any_path)
|
|
115
|
+
if fs_path.is_dir():
|
|
116
|
+
return fs_path
|
|
117
|
+
|
|
118
|
+
if fs_path.is_file():
|
|
119
|
+
tmpdir = Path(tempfile.mkdtemp())
|
|
120
|
+
sys.stdout.write(f">> Extracting PULL SDK files from zip archive '{fs_path}' to directory '{tmpdir}'\n")
|
|
121
|
+
with zipfile.ZipFile(str(fs_path), "r") as zip_ref:
|
|
122
|
+
zip_ref.extractall(tmpdir)
|
|
123
|
+
|
|
124
|
+
return tmpdir
|
|
125
|
+
|
|
126
|
+
raise StepFailedError(f"File or directory '{any_path}' not found")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _install_dlls(source_dir: Path, lib_root: Path, use_uac: bool) -> None:
|
|
130
|
+
# Check if all required dlls are present in the source directory
|
|
131
|
+
sdk_found = all((source_dir / f).exists() for f in PULL_SDK_DLLS)
|
|
132
|
+
# Archive from the official website contains dlls in a subdirectory
|
|
133
|
+
if not sdk_found:
|
|
134
|
+
subdirs = list(source_dir.glob(PULL_SDK_SUBDIR_GLOB))
|
|
135
|
+
if subdirs:
|
|
136
|
+
source_dir = Path(subdirs[0])
|
|
137
|
+
sdk_found = all((source_dir / f).exists() for f in PULL_SDK_DLLS)
|
|
138
|
+
|
|
139
|
+
if not sdk_found:
|
|
140
|
+
raise StepFailedError(
|
|
141
|
+
f"PULL SDK not found in '{source_dir}'. \n"
|
|
142
|
+
f"Please check the path contains the following files: {PULL_SDK_DLLS}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
files_glob = Path("*.dll")
|
|
146
|
+
sys.stdout.write(f">> Copying PULL SDK from {source_dir / files_glob} to {lib_root}...\n")
|
|
147
|
+
_copy_files(source_dir, files_glob, lib_root, use_uac)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ShellExecuteInfo(ctypes.Structure):
|
|
151
|
+
_fields_ = [
|
|
152
|
+
("cbSize", DWORD),
|
|
153
|
+
("fMask", c_ulong),
|
|
154
|
+
("hwnd", HWND),
|
|
155
|
+
("lpVerb", c_char_p),
|
|
156
|
+
("lpFile", c_char_p),
|
|
157
|
+
("lpParameters", c_char_p),
|
|
158
|
+
("lpDirectory", c_char_p),
|
|
159
|
+
("nShow", c_int),
|
|
160
|
+
("hInstApp", HINSTANCE),
|
|
161
|
+
("lpIDList", c_void_p),
|
|
162
|
+
("lpClass", c_char_p),
|
|
163
|
+
("hKeyClass", HKEY),
|
|
164
|
+
("dwHotKey", DWORD),
|
|
165
|
+
("hIcon", HANDLE),
|
|
166
|
+
("hProcess", HANDLE),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
def __init__(self, **kw: Any) -> None:
|
|
170
|
+
super().__init__()
|
|
171
|
+
self.cbSize = ctypes.sizeof(self) # pylint: disable=invalid-name
|
|
172
|
+
for field_name, field_value in kw.items():
|
|
173
|
+
setattr(self, field_name, field_value)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _copy_files(source_dir: Path, files_glob: Path, destination: Path, use_uac: bool) -> None:
|
|
177
|
+
files = list(source_dir.glob(str(files_glob)))
|
|
178
|
+
if not windll.shell32.IsUserAnAdmin() and use_uac:
|
|
179
|
+
# Run command with showing UAC prompt
|
|
180
|
+
# The `copy` command does not support copying several files at once, so pass the glob pattern
|
|
181
|
+
sys.stdout.write(">> Copying with elevated permissions...\n")
|
|
182
|
+
_elevated_command("cmd", ["/c", "copy", "/Y", str(source_dir / files_glob), str(destination)])
|
|
183
|
+
sys.stdout.write("\n".join(f">>> {f}" for f in files) + "\n")
|
|
184
|
+
else:
|
|
185
|
+
for file in files:
|
|
186
|
+
sys.stdout.write(f">>> {file}\n")
|
|
187
|
+
shutil.copy(file, destination)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# Windows stuff
|
|
191
|
+
|
|
192
|
+
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
|
193
|
+
SEE_MASK_NO_CONSOLE = 0x00008000
|
|
194
|
+
|
|
195
|
+
PShellExecuteInfo = POINTER(ShellExecuteInfo)
|
|
196
|
+
|
|
197
|
+
ShellExecuteEx = windll.shell32.ShellExecuteExA
|
|
198
|
+
ShellExecuteEx.argtypes = (PShellExecuteInfo,)
|
|
199
|
+
ShellExecuteEx.restype = BOOL
|
|
200
|
+
|
|
201
|
+
WaitForSingleObject = windll.kernel32.WaitForSingleObject
|
|
202
|
+
WaitForSingleObject.argtypes = (HANDLE, DWORD)
|
|
203
|
+
WaitForSingleObject.restype = DWORD
|
|
204
|
+
|
|
205
|
+
CloseHandle = windll.kernel32.CloseHandle
|
|
206
|
+
CloseHandle.argtypes = (HANDLE,)
|
|
207
|
+
CloseHandle.restype = BOOL
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _elevated_command(command: str, args: Iterable[str]) -> None:
|
|
211
|
+
|
|
212
|
+
params = ShellExecuteInfo(
|
|
213
|
+
fMask=SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
|
|
214
|
+
hwnd=None,
|
|
215
|
+
lpVerb=b"runas",
|
|
216
|
+
lpFile=command.encode("cp1252"),
|
|
217
|
+
lpParameters=subprocess.list2cmdline(args).encode("cp1252"),
|
|
218
|
+
nShow=1,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if not ShellExecuteEx(ctypes.byref(params)):
|
|
222
|
+
raise ctypes.WinError()
|
|
223
|
+
|
|
224
|
+
handle = params.hProcess
|
|
225
|
+
ret = DWORD()
|
|
226
|
+
WaitForSingleObject(handle, -1)
|
|
227
|
+
|
|
228
|
+
if windll.kernel32.GetExitCodeProcess(handle, ctypes.byref(ret)) == 0:
|
|
229
|
+
raise ctypes.WinError()
|
|
230
|
+
|
|
231
|
+
CloseHandle(handle)
|
pyzkaccess/aux_input.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
__all__ = ["AuxInput", "AuxInputList"]
|
|
2
|
+
from abc import ABCMeta, abstractmethod
|
|
3
|
+
from typing import Any, Iterable, TypeVar, Union, overload
|
|
4
|
+
|
|
5
|
+
from pyzkaccess.common import UserTuple
|
|
6
|
+
from pyzkaccess.event import EventLog
|
|
7
|
+
from pyzkaccess.sdk import ZKSDK
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuxInputInterface(metaclass=ABCMeta):
|
|
11
|
+
event_types = (220, 221)
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def events(self) -> EventLog:
|
|
15
|
+
"""Event log of current aux input"""
|
|
16
|
+
return self._specific_event_log()
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def _specific_event_log(self) -> EventLog:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuxInput(AuxInputInterface):
|
|
24
|
+
"""An auxiliary input"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, sdk: ZKSDK, event_log: EventLog, number: int):
|
|
27
|
+
self.number = number
|
|
28
|
+
self._sdk = sdk
|
|
29
|
+
self._event_log = event_log
|
|
30
|
+
|
|
31
|
+
def _specific_event_log(self) -> EventLog:
|
|
32
|
+
return self._event_log.only(door=[self.number], event_type=self.event_types)
|
|
33
|
+
|
|
34
|
+
def __eq__(self, other: Any) -> bool:
|
|
35
|
+
if isinstance(other, AuxInput):
|
|
36
|
+
return self.number == other.number and self._sdk is other._sdk
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def __ne__(self, other: Any) -> bool:
|
|
40
|
+
return not self.__eq__(other)
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return f"AuxInput[{self.number}]"
|
|
44
|
+
|
|
45
|
+
def __repr__(self) -> str:
|
|
46
|
+
return self.__str__()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_AuxInputListT = TypeVar("_AuxInputListT", bound="AuxInputList")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuxInputList(AuxInputInterface, UserTuple[AuxInput]):
|
|
53
|
+
"""Auxiliary input collection for group operations"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, sdk: ZKSDK, event_log: EventLog, aux_inputs: Iterable[AuxInput] = ()):
|
|
56
|
+
super().__init__(aux_inputs)
|
|
57
|
+
self._sdk = sdk
|
|
58
|
+
self._event_log = event_log
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def __getitem__(self, item: int) -> AuxInput:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def __getitem__(self: _AuxInputListT, item: slice) -> _AuxInputListT:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def __getitem__(self: _AuxInputListT, item: Union[int, slice]) -> Union[AuxInput, _AuxInputListT]:
|
|
69
|
+
if isinstance(item, slice):
|
|
70
|
+
return self.__class__(self._sdk, self._event_log, aux_inputs=self.data[item])
|
|
71
|
+
|
|
72
|
+
return self.data[item]
|
|
73
|
+
|
|
74
|
+
def _specific_event_log(self) -> EventLog:
|
|
75
|
+
doors = set(x.number for x in self)
|
|
76
|
+
return self._event_log.only(door=doors, event_type=self.event_types)
|