pyezvizapi 1.0.1.7__py3-none-any.whl → 1.0.1.9__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 pyezvizapi might be problematic. Click here for more details.

@@ -1,149 +1,135 @@
1
1
  """Test camera RTSP authentication."""
2
+
3
+ from __future__ import annotations
4
+
2
5
  import base64
3
6
  import hashlib
4
7
  import socket
8
+ from typing import TypedDict
5
9
 
6
10
  from .exceptions import AuthTestResultFailed, InvalidHost
7
11
 
8
12
 
9
- def genmsg_describe(url, seq, user_agent, auth_seq):
10
- """Generate RTSP describe message."""
11
- msg_ret = "DESCRIBE " + url + " RTSP/1.0\r\n"
12
- msg_ret += "CSeq: " + str(seq) + "\r\n"
13
- msg_ret += "Authorization: " + auth_seq + "\r\n"
14
- msg_ret += "User-Agent: " + user_agent + "\r\n"
15
- msg_ret += "Accept: application/sdp\r\n"
16
- msg_ret += "\r\n"
13
+ def genmsg_describe(url: str, seq: int, user_agent: str, auth_seq: str) -> str:
14
+ """Generate RTSP DESCRIBE request message."""
15
+ msg_ret = f"DESCRIBE {url} RTSP/1.0\r\n"
16
+ msg_ret += f"CSeq: {seq}\r\n"
17
+ msg_ret += f"Authorization: {auth_seq}\r\n"
18
+ msg_ret += f"User-Agent: {user_agent}\r\n"
19
+ msg_ret += "Accept: application/sdp\r\n\r\n"
17
20
  return msg_ret
18
21
 
19
22
 
23
+ class RTSPDetails(TypedDict):
24
+ """Typed structure for RTSP test parameters."""
25
+ bufLen: int
26
+ defaultServerIp: str
27
+ defaultServerPort: int
28
+ defaultTestUri: str
29
+ defaultUserAgent: str
30
+ defaultUsername: str | None
31
+ defaultPassword: str | None
32
+
33
+
20
34
  class TestRTSPAuth:
21
- """Test RTSP credentials."""
35
+ """Test RTSP credentials against an RTSP server."""
36
+
37
+ _rtsp_details: RTSPDetails
22
38
 
23
39
  def __init__(
24
- self,
25
- ip_addr,
26
- username=None,
27
- password=None,
28
- test_uri="",
40
+ self, ip_addr: str, username: str | None = None, password: str | None = None, test_uri: str = ""
29
41
  ) -> None:
30
42
  """Initialize RTSP credential test."""
31
- self._rtsp_details = {
32
- "bufLen": 1024,
33
- "defaultServerIp": ip_addr,
34
- "defaultServerPort": 554,
35
- "defaultTestUri": test_uri,
36
- "defaultUserAgent": "RTSP Client",
37
- "defaultUsername": username,
38
- "defaultPassword": password,
39
- }
40
-
41
- def generate_auth_string(self, realm, method, uri, nonce):
42
- """Generate digest auth string."""
43
- map_return_info = {}
43
+ self._rtsp_details = RTSPDetails(
44
+ bufLen=1024,
45
+ defaultServerIp=ip_addr,
46
+ defaultServerPort=554,
47
+ defaultTestUri=test_uri,
48
+ defaultUserAgent="RTSP Client",
49
+ defaultUsername=username,
50
+ defaultPassword=password,
51
+ )
52
+
53
+ def generate_auth_string(self, realm: bytes, method: str, uri: str, nonce: bytes) -> str:
54
+ """Generate the HTTP Digest Authorization header value."""
44
55
  m_1 = hashlib.md5(
45
- f"{self._rtsp_details['defaultUsername']}:"
46
- f"{realm.decode()}:"
47
- f"{self._rtsp_details['defaultPassword']}".encode()
56
+ f"{self._rtsp_details['defaultUsername']}:{realm.decode()}:{self._rtsp_details['defaultPassword']}".encode()
48
57
  ).hexdigest()
49
58
  m_2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
50
- response = hashlib.md5(f"{m_1}:{nonce}:{m_2}".encode()).hexdigest()
59
+ response = hashlib.md5(f"{m_1}:{nonce.decode()}:{m_2}".encode()).hexdigest()
51
60
 
52
- map_return_info = (
53
- f"Digest "
61
+ return (
62
+ "Digest "
54
63
  f"username=\"{self._rtsp_details['defaultUsername']}\", "
55
- f'realm="{realm.decode()}", '
56
- f'algorithm="MD5", '
57
- f'nonce="{nonce.decode()}", '
58
- f'uri="{uri}", '
59
- f'response="{response}"'
64
+ f"realm=\"{realm.decode()}\", "
65
+ 'algorithm="MD5", '
66
+ f"nonce=\"{nonce.decode()}\", "
67
+ f"uri=\"{uri}\", "
68
+ f"response=\"{response}\""
60
69
  )
61
- return map_return_info
62
70
 
63
- def main(self):
64
- """Start main method."""
65
- session = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
71
+ def main(self) -> None:
72
+ """Open RTSP socket, try Basic and then Digest auth for DESCRIBE."""
73
+ session: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
66
74
 
67
75
  try:
68
76
  session.connect(
69
- (
70
- self._rtsp_details["defaultServerIp"],
71
- self._rtsp_details["defaultServerPort"],
72
- )
77
+ (self._rtsp_details["defaultServerIp"], self._rtsp_details["defaultServerPort"])
73
78
  )
74
-
75
79
  except TimeoutError as err:
76
80
  raise AuthTestResultFailed("Invalid ip or camera hibernating") from err
77
-
78
81
  except (socket.gaierror, ConnectionRefusedError) as err:
79
82
  raise InvalidHost("Invalid IP or Hostname") from err
80
83
 
81
- seq = 1
84
+ seq: int = 1
82
85
 
83
- url = (
84
- "rtsp://"
85
- + self._rtsp_details["defaultServerIp"]
86
- + self._rtsp_details["defaultTestUri"]
87
- )
88
-
89
- auth_seq = base64.b64encode(
90
- f"{self._rtsp_details['defaultUsername']}:"
91
- f"{self._rtsp_details['defaultPassword']}".encode("ascii")
92
- )
93
- auth_seq = "Basic " + auth_seq.decode()
86
+ url: str = "rtsp://" + self._rtsp_details["defaultServerIp"] + self._rtsp_details["defaultTestUri"]
94
87
 
95
- print(
96
- genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
88
+ # Basic Authorization header
89
+ auth_b64: bytes = base64.b64encode(
90
+ f"{self._rtsp_details['defaultUsername']}:{self._rtsp_details['defaultPassword']}".encode("ascii")
97
91
  )
98
- session.send(
99
- genmsg_describe(
100
- url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
101
- ).encode()
102
- )
103
- msg1 = session.recv(self._rtsp_details["bufLen"])
104
- seq = seq + 1
105
-
106
- if msg1.decode().find("200 OK") > 1:
107
- print(f"Basic auth result: {msg1.decode()}")
108
- return print("Basic Auth test passed. Credentials Valid!")
109
-
110
- if msg1.decode().find("Unauthorized") > 1:
111
- # Basic failed, doing new DESCRIBE with digest authentication.
112
- start = msg1.decode().find("realm")
113
- begin = msg1.decode().find('"', start)
114
- end = msg1.decode().find('"', begin + 1)
115
- realm = msg1[begin + 1 : end]
116
-
117
- start = msg1.decode().find("nonce")
118
- begin = msg1.decode().find('"', start)
119
- end = msg1.decode().find('"', begin + 1)
120
- nonce = msg1[begin + 1 : end]
121
-
122
- auth_seq = self.generate_auth_string(
123
- realm,
124
- "DESCRIBE",
125
- self._rtsp_details["defaultTestUri"],
126
- nonce,
127
- )
128
-
129
- print(
130
- genmsg_describe(
131
- url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
132
- )
133
- )
134
-
135
- session.send(
136
- genmsg_describe(
137
- url, seq, self._rtsp_details["defaultUserAgent"], auth_seq
138
- ).encode()
139
- )
92
+ auth_seq: str = "Basic " + auth_b64.decode()
93
+
94
+ describe = genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
95
+ print(describe)
96
+ session.send(describe.encode())
97
+ msg1: bytes = session.recv(self._rtsp_details["bufLen"])
98
+ seq += 1
99
+
100
+ decoded = msg1.decode()
101
+ if "200 OK" in decoded:
102
+ print(f"Basic auth result: {decoded}")
103
+ print("Basic Auth test passed. Credentials Valid!")
104
+ return
105
+
106
+ if "Unauthorized" in decoded:
107
+ # Basic failed, do new DESCRIBE with digest authentication.
108
+ start = decoded.find("realm")
109
+ begin = decoded.find('"', start)
110
+ end = decoded.find('"', begin + 1)
111
+ realm: bytes = msg1[begin + 1 : end]
112
+
113
+ start = decoded.find("nonce")
114
+ begin = decoded.find('"', start)
115
+ end = decoded.find('"', begin + 1)
116
+ nonce: bytes = msg1[begin + 1 : end]
117
+
118
+ auth_seq = self.generate_auth_string(realm, "DESCRIBE", self._rtsp_details["defaultTestUri"], nonce)
119
+
120
+ describe = genmsg_describe(url, seq, self._rtsp_details["defaultUserAgent"], auth_seq)
121
+ print(describe)
122
+ session.send(describe.encode())
140
123
  msg1 = session.recv(self._rtsp_details["bufLen"])
141
- print(f"Digest auth result: {msg1.decode()}")
124
+ decoded = msg1.decode()
125
+ print(f"Digest auth result: {decoded}")
142
126
 
143
- if msg1.decode().find("200 OK") > 1:
144
- return print("Digest Auth test Passed. Credentials Valid!")
127
+ if "200 OK" in decoded:
128
+ print("Digest Auth test Passed. Credentials Valid!")
129
+ return
145
130
 
146
- if msg1.decode().find("401 Unauthorized") > 1:
131
+ if "401 Unauthorized" in decoded:
147
132
  raise AuthTestResultFailed("Credentials not valid!!")
148
133
 
149
- return print("Basic Auth test passed. Credentials Valid!")
134
+ print("Basic Auth test passed. Credentials Valid!")
135
+ # ruff: noqa: T201
pyezvizapi/test_mqtt.py CHANGED
@@ -1,64 +1,135 @@
1
- """MQTT test module."""
1
+ """MQTT test module.
2
2
 
3
+ Run a simple MQTT listener using either a saved token file
4
+ (`--token-file ezviz_token.json`) or by prompting for username/password
5
+ with MFA similar to the main CLI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
3
11
  from getpass import getpass
4
12
  import json
5
13
  import logging
6
14
  from pathlib import Path
15
+ import sys
7
16
  import time
8
- from typing import Any
17
+ from typing import Any, cast
9
18
 
10
- from .client import EzvizClient # Your login client
11
- from .mqtt import MQTTClient # The refactored MQTT class
19
+ from .client import EzvizClient
20
+ from .exceptions import EzvizAuthVerificationCode, PyEzvizError
12
21
 
13
- logging.basicConfig(level=logging.INFO)
22
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
14
23
 
15
24
  LOG_FILE = Path("mqtt_messages.jsonl") # JSON Lines format
16
25
 
17
26
 
18
27
  def message_handler(msg: dict[str, Any]) -> None:
19
- """Handle new MQTT messages by printing and saving them to a file.
20
-
21
- Args:
22
- msg (Dict[str, Any]): The decoded MQTT message.
23
- """
28
+ """Handle new MQTT messages by printing and saving them to a file."""
24
29
  print("📩 New MQTT message:", msg)
25
-
26
- # Append to JSONL file
27
30
  with LOG_FILE.open("a", encoding="utf-8") as f:
28
31
  f.write(json.dumps(msg, ensure_ascii=False) + "\n")
29
32
 
30
33
 
31
- def main() -> None:
32
- """Entry point for testing MQTT messages.
33
-
34
- Prompts for username and password, logs into Ezviz, starts MQTT listener,
35
- and writes incoming messages to a JSONL file.
36
- """
37
- # Prompt for credentials
38
- username = input("Ezviz username: ")
39
- password = getpass("Ezviz password: ")
34
+ def _load_token_file(path: str | None) -> dict[str, Any] | None:
35
+ if not path:
36
+ return None
37
+ p = Path(path)
38
+ if not p.exists():
39
+ return None
40
+ try:
41
+ return cast(dict[str, Any], json.loads(p.read_text(encoding="utf-8")))
42
+ except (OSError, json.JSONDecodeError):
43
+ logging.getLogger(__name__).warning("Failed to read token file: %s", p)
44
+ return None
40
45
 
41
- # Step 1: Log into Ezviz to get a token
42
- client = EzvizClient(account=username, password=password)
43
- client.login()
44
46
 
45
- # Step 2: Start MQTT client
46
- mqtt_client: MQTTClient = client.get_mqtt_client(
47
- on_message_callback=message_handler
47
+ def _save_token_file(path: str | None, token: dict[str, Any]) -> None:
48
+ if not path:
49
+ return
50
+ p = Path(path)
51
+ try:
52
+ p.write_text(json.dumps(token, indent=2), encoding="utf-8")
53
+ logging.getLogger(__name__).info("Saved token to %s", p)
54
+ except OSError:
55
+ logging.getLogger(__name__).warning("Failed to save token file: %s", p)
56
+
57
+
58
+ def main(argv: list[str] | None = None) -> int:
59
+ """Entry point for testing MQTT messages."""
60
+ parser = argparse.ArgumentParser(prog="test_mqtt")
61
+ parser.add_argument("-u", "--username", required=False, help="Ezviz username")
62
+ parser.add_argument("-p", "--password", required=False, help="Ezviz password")
63
+ parser.add_argument(
64
+ "-r",
65
+ "--region",
66
+ required=False,
67
+ default="apiieu.ezvizlife.com",
68
+ help="Ezviz API region",
69
+ )
70
+ parser.add_argument(
71
+ "--token-file",
72
+ type=str,
73
+ default="ezviz_token.json",
74
+ help="Path to JSON token file (default: ezviz_token.json)",
75
+ )
76
+ parser.add_argument(
77
+ "--save-token",
78
+ action="store_true",
79
+ help="Save token to --token-file after successful login",
48
80
  )
81
+ args = parser.parse_args(argv)
82
+
83
+ token = _load_token_file(args.token_file)
84
+
85
+ username = args.username
86
+ password = args.password
87
+
88
+ # If no token and missing username/password, prompt interactively
89
+ if not token and (not username or not password):
90
+ print("No token found. Please enter Ezviz credentials.")
91
+ if not username:
92
+ username = input("Username: ")
93
+ if not password:
94
+ password = getpass("Password: ")
95
+
96
+ client = EzvizClient(username, password, args.region, token=token)
97
+
98
+ # Login if we have credentials (to refresh session and populate service URLs)
99
+ if username and password:
100
+ try:
101
+ client.login()
102
+ except EzvizAuthVerificationCode:
103
+ mfa_code = input("MFA code required, please input MFA code.\n")
104
+ try:
105
+ code_int = int(mfa_code.strip())
106
+ except ValueError:
107
+ code_int = None
108
+ client.login(sms_code=code_int)
109
+ except PyEzvizError as exp:
110
+ print(f"Login failed: {exp}")
111
+ return 1
112
+
113
+ # Start MQTT client
114
+ mqtt_client = client.get_mqtt_client(on_message_callback=message_handler)
49
115
  mqtt_client.connect()
50
116
 
51
117
  try:
52
118
  print("Listening for MQTT messages... (Ctrl+C to quit)")
53
119
  while True:
54
- time.sleep(1) # Keep process alive
120
+ time.sleep(1)
55
121
  except KeyboardInterrupt:
56
122
  print("\nStopping...")
57
123
  finally:
58
124
  mqtt_client.stop()
59
125
  print("Stopped.")
60
126
 
127
+ if args.save_token and args.token_file:
128
+ _save_token_file(args.token_file, cast(dict[str, Any], client._token)) # noqa: SLF001
129
+
130
+ return 0
61
131
 
62
- if __name__ == "__main__":
63
- main()
64
132
 
133
+ if __name__ == "__main__":
134
+ sys.exit(main())
135
+ # ruff: noqa: T201
pyezvizapi/utils.py CHANGED
@@ -83,7 +83,6 @@ def decrypt_image(input_data: bytes, password: str) -> bytes:
83
83
  bytes: Decrypted image data
84
84
 
85
85
  """
86
-
87
86
  if len(input_data) < 48:
88
87
  raise PyEzvizError("Invalid image data")
89
88
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyezvizapi
3
- Version: 1.0.1.7
3
+ Version: 1.0.1.9
4
4
  Summary: Pilot your Ezviz cameras
5
5
  Home-page: https://github.com/RenierM26/pyEzvizApi/
6
6
  Author: Renier Moorcroft
7
7
  Author-email: RenierM26@users.github.com
8
8
  License: Apache Software License 2.0
9
- Requires-Python: >=3.6
9
+ Requires-Python: >=3.11
10
10
  License-File: LICENSE
11
11
  License-File: LICENSE.md
12
12
  Requires-Dist: requests
@@ -0,0 +1,21 @@
1
+ pyezvizapi/__init__.py,sha256=IDnIN_nfIISVwuy0cVBh4wspgAav6MuOJCQGajjyU3g,1881
2
+ pyezvizapi/__main__.py,sha256=SeV954H-AV-U1thNxRd7rWTGtSlfWyNzdrjF8gikYus,20777
3
+ pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
4
+ pyezvizapi/camera.py,sha256=Vpuh7RkUBfSmNCFAXaALzfvvL0RD3SzJJyWqwZzWuHk,25191
5
+ pyezvizapi/cas.py,sha256=ISmb-eTPuacI10L87lULbQ-oDMBuygO7Tf-s9f-tvYE,5995
6
+ pyezvizapi/client.py,sha256=dcHAsdtfSTkVCrkaEZKpFggDXX4t5debpuxc76sOy-k,71267
7
+ pyezvizapi/constants.py,sha256=R0zGg8Rv59An36dSXQLl3WU7VbpTbpo2comk9VJc68k,12535
8
+ pyezvizapi/exceptions.py,sha256=8rmxEUQdrziqMe-M1SeeRd0HtP2IDQ2xpJVj7wvOQyo,976
9
+ pyezvizapi/light_bulb.py,sha256=9wgycG3dTvBbrsxQjQnXal-GA8VXPsIN1m-CTtRh8i0,7797
10
+ pyezvizapi/models.py,sha256=NQzwTP0yEe2IWU-Vc6nAn87xulpTuo0MX2Rcf0WxifA,4176
11
+ pyezvizapi/mqtt.py,sha256=aOL-gexZgYvCCaNQ03M4vZan91d5p2Fl_qsFykn9NW4,22365
12
+ pyezvizapi/test_cam_rtsp.py,sha256=WGSM5EiOTl_r1mWHoMb7bXHm_BCn1P9X_669YQ38r6k,4903
13
+ pyezvizapi/test_mqtt.py,sha256=Orn-fwZPJIE4G5KROMX0MRAkLwU6nLb9LUtXyb2ZCQs,4147
14
+ pyezvizapi/utils.py,sha256=o342o3LI9eP8qla1vXM2rqlVbdTiLK0dAqrkyUSXpg8,5939
15
+ pyezvizapi-1.0.1.9.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
+ pyezvizapi-1.0.1.9.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
17
+ pyezvizapi-1.0.1.9.dist-info/METADATA,sha256=kZWGIVtf7BcQ1WJRCFCDQZmh1NibUNQs_S6TONkU-CQ,695
18
+ pyezvizapi-1.0.1.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ pyezvizapi-1.0.1.9.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
20
+ pyezvizapi-1.0.1.9.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
21
+ pyezvizapi-1.0.1.9.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- pyezvizapi/__init__.py,sha256=ByMIDi_hdJa7Sc7GoJmviTOBe4DILx2E4UzhEpEzQis,1422
2
- pyezvizapi/__main__.py,sha256=OsawbYa-eoLUTOMr4xWeryq4sDzs09WL9GELKQTbda8,16396
3
- pyezvizapi/api_endpoints.py,sha256=rk6VinLVCn-B6DxnhfV79liplNpgUsipNbTEa_MRVwU,2755
4
- pyezvizapi/camera.py,sha256=BpmLySbnWWEKimSwgjx_cB60Q-6dbgdY-NA--NzUvps,11486
5
- pyezvizapi/cas.py,sha256=d31ZflYSD9P40MnsNRNZbT0HVLvlnHokKpLbdAjWQ74,5631
6
- pyezvizapi/client.py,sha256=uhtQ4bdM0zmnAPEt3cvxGtrqLFf0C15A5zIBE719pDE,86179
7
- pyezvizapi/constants.py,sha256=ntH7gNNRHQ66Dek2Uhk9PD4wXT0h7QTDn929LFGSP9I,12333
8
- pyezvizapi/exceptions.py,sha256=28lLyM0ILTRHgWqr9D-DqqKFXx7POuF0WAZctdC8Kbc,735
9
- pyezvizapi/light_bulb.py,sha256=ADLrPZ6NL4vANzmohU63QuD9qVGkKHkX9C0o7Evbv-A,5730
10
- pyezvizapi/mqtt.py,sha256=9i2dkVuNgf9KB2b-58HqHuKIzl-Ouuodg0dJ0DYpUOo,21649
11
- pyezvizapi/test_cam_rtsp.py,sha256=w7GPcYIeK78TxL8zFDihdGSDQNWcYrurwZOr6uFzzgo,4902
12
- pyezvizapi/test_mqtt.py,sha256=6thgcsfvl-wSR2Xrp8miG7PHfg9Q-a4AZ8VvOhEsBBQ,1651
13
- pyezvizapi/utils.py,sha256=zjbvQiJ_Q-qwbB_FImXVjuaRct9cogBRKDxzFHbx4ek,5940
14
- pyezvizapi-1.0.1.7.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
15
- pyezvizapi-1.0.1.7.dist-info/licenses/LICENSE.md,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
16
- pyezvizapi-1.0.1.7.dist-info/METADATA,sha256=e4GDIcD5BMofWTvFXl6EIUpF-2vrRGp1n9VTMZCOKPs,694
17
- pyezvizapi-1.0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- pyezvizapi-1.0.1.7.dist-info/entry_points.txt,sha256=_BSJ3eNb2H_AZkRdsv1s4mojqWn3N7m503ujvg1SudA,56
19
- pyezvizapi-1.0.1.7.dist-info/top_level.txt,sha256=gMZTelIi8z7pXyTCQLLaIkxVRrDQ_lS2NEv0WgfHrHs,11
20
- pyezvizapi-1.0.1.7.dist-info/RECORD,,