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 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)
@@ -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)