pypetkitapi 1.10.1__tar.gz → 1.10.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.10.1
3
+ Version: 1.10.3
4
4
  Summary: Python client for PetKit API
5
5
  License: MIT
6
6
  Author: Jezza34000
@@ -51,7 +51,7 @@ from .media import DownloadDecryptMedia, MediaCloud, MediaFile, MediaManager
51
51
  from .purifier_container import Purifier
52
52
  from .water_fountain_container import WaterFountain
53
53
 
54
- __version__ = "1.10.1"
54
+ __version__ = "1.10.3"
55
55
 
56
56
  __all__ = [
57
57
  "CTW3",
@@ -108,8 +108,9 @@ class PetKitClient:
108
108
  _LOGGER.debug("Getting API server list")
109
109
  self.req.base_url = PetkitDomain.PASSPORT_PETKIT
110
110
 
111
- if self.region.lower() == "china":
111
+ if self.region.lower() == "china" or self.region.lower() == "cn":
112
112
  self.req.base_url = PetkitDomain.CHINA_SRV
113
+ _LOGGER.debug("Using specific China server: %s", PetkitDomain.CHINA_SRV)
113
114
  return
114
115
 
115
116
  response = await self.req.request(
@@ -451,7 +451,7 @@ class Litter(BaseModel):
451
451
  service_status: int | None = Field(None, alias="serviceStatus")
452
452
  total_time: int | None = Field(None, alias="totalTime")
453
453
  with_k3: int | None = Field(None, alias="withK3")
454
- User: UserDevice | None = None
454
+ user: UserDevice | None = None
455
455
  device_records: list[LitterRecord] | None = None
456
456
  device_stats: LitterStats | None = None
457
457
  device_pet_graph_out: list[PetOutGraph] | None = None
@@ -31,8 +31,8 @@ _LOGGER = logging.getLogger(__name__)
31
31
 
32
32
  @dataclass
33
33
  class MediaCloud:
34
- """Dataclass MediaFile.
35
- Represents a media file from a PetKit device.
34
+ """Dataclass MediaCloud.
35
+ Represents a media file from Petkit API.
36
36
  """
37
37
 
38
38
  event_id: str
@@ -68,7 +68,10 @@ class MediaManager:
68
68
  async def get_all_media_files(
69
69
  self, devices: list[Feeder | Litter]
70
70
  ) -> list[MediaCloud]:
71
- """Get all media files from all devices and return a list of MediaFile."""
71
+ """Get all media files from all devices and return a list of MediaCloud.
72
+ :param devices: List of devices
73
+ :return: List of MediaCloud objects
74
+ """
72
75
  media_files: list[MediaCloud] = []
73
76
  _LOGGER.debug("Processing media files for %s devices", len(devices))
74
77
 
@@ -101,8 +104,10 @@ class MediaManager:
101
104
  async def get_all_media_files_disk(
102
105
  self, storage_path: Path, device_id: int
103
106
  ) -> None:
104
- """Construct the media file table for disk storage."""
105
-
107
+ """Construct the media file table for disk storage.
108
+ :param storage_path: Path to the storage directory
109
+ :param device_id: Device ID
110
+ """
106
111
  self.media_table.clear()
107
112
 
108
113
  today_str = datetime.now().strftime("%Y%m%d")
@@ -114,7 +119,7 @@ class MediaManager:
114
119
  video_path = record_path / "video"
115
120
 
116
121
  # Regex pattern to match valid filenames
117
- valid_pattern = re.compile(rf"^{device_id}_\d+\.(jpg|avi)$")
122
+ valid_pattern = re.compile(rf"^(?:\d+_)?{device_id}_\d+\.(jpg|avi)$")
118
123
 
119
124
  # Populate the media table with event_id from filenames
120
125
  for subdir in [snapshot_path, video_path]:
@@ -130,7 +135,7 @@ class MediaManager:
130
135
  if entry.is_file() and valid_pattern.match(entry.name):
131
136
  _LOGGER.debug("Entries found: %s", entry.name)
132
137
  event_id = Path(entry.name).stem
133
- timestamp = Path(entry.name).stem.split("_", 1)[1]
138
+ timestamp = self._extract_timestamp(str(entry.name))
134
139
  media_type_str = Path(entry.name).suffix.lstrip(".")
135
140
  try:
136
141
  media_type = MediaType(media_type_str)
@@ -148,13 +153,29 @@ class MediaManager:
148
153
  )
149
154
  )
150
155
 
156
+ @staticmethod
157
+ def _extract_timestamp(file_name: str) -> int:
158
+ """Extract timestamp from a filename.
159
+ :param file_name: Filename
160
+ :return: Timestamp
161
+ """
162
+ match = re.search(r"_(\d+)\.[a-zA-Z0-9]+$", file_name)
163
+ if match:
164
+ return int(match.group(1))
165
+ return 0
166
+
151
167
  async def prepare_missing_files(
152
168
  self,
153
169
  media_cloud_list: list[MediaCloud],
154
170
  dl_type: list[MediaType] | None = None,
155
171
  event_type: list[RecordType] | None = None,
156
172
  ) -> list[MediaCloud]:
157
- """Compare MediaCloud objects with MediaFile objects and return a list of missing MediaCloud objects."""
173
+ """Compare MediaCloud objects with MediaFile objects and return a list of missing MediaCloud objects.
174
+ :param media_cloud_list: List of MediaCloud objects
175
+ :param dl_type: List of media types to download
176
+ :param event_type: List of event types to filter
177
+ :return: List of missing MediaCloud objects
178
+ """
158
179
  missing_media = []
159
180
  existing_event_ids = {media_file.event_id for media_file in self.media_table}
160
181
 
@@ -199,7 +220,10 @@ class MediaManager:
199
220
  return missing_media
200
221
 
201
222
  def _process_feeder(self, feeder: Feeder) -> list[MediaCloud]:
202
- """Process media files for a Feeder device."""
223
+ """Process media files for a Feeder device.
224
+ :param feeder: Feeder device object
225
+ :return: List of MediaCloud objects for the device
226
+ """
203
227
  media_files: list[MediaCloud] = []
204
228
  records = feeder.device_records
205
229
 
@@ -219,7 +243,12 @@ class MediaManager:
219
243
  def _process_feeder_record(
220
244
  self, record, record_type: RecordType, device_obj: Feeder
221
245
  ) -> list[MediaCloud]:
222
- """Process individual feeder records."""
246
+ """Process individual feeder records.
247
+ :param record: Record object
248
+ :param record_type: Record type
249
+ :param device_obj: Feeder device object
250
+ :return: List of MediaCloud objects for the record
251
+ """
223
252
  media_files: list[MediaCloud] = []
224
253
  user_id = device_obj.user.id if device_obj.user else None
225
254
  feeder_id = device_obj.device_nfo.device_id if device_obj.device_nfo else None
@@ -277,7 +306,10 @@ class MediaManager:
277
306
  return media_files
278
307
 
279
308
  def _process_litter(self, litter: Litter) -> list[MediaCloud]:
280
- """Process media files for a Litter device."""
309
+ """Process media files for a Litter device.
310
+ :param litter: Litter device object
311
+ :return: List of MediaCloud objects for the device
312
+ """
281
313
  media_files: list[MediaCloud] = []
282
314
  records = litter.device_records
283
315
  litter_id = litter.device_nfo.device_id if litter.device_nfo else None
@@ -339,7 +371,13 @@ class MediaManager:
339
371
  def construct_video_url(
340
372
  device_type: str | None, media_url: str | None, user_id: int, cp_sub: int | None
341
373
  ) -> str | None:
342
- """Construct the video URL."""
374
+ """Construct the video URL.
375
+ :param device_type: Device type
376
+ :param media_url: Media URL
377
+ :param user_id: User ID
378
+ :param cp_sub: Cpsub value
379
+ :return: Constructed video URL
380
+ """
343
381
  if not media_url or not user_id or cp_sub != 1:
344
382
  return None
345
383
  params = parse_qs(urlparse(media_url).query)
@@ -348,7 +386,10 @@ class MediaManager:
348
386
 
349
387
  @staticmethod
350
388
  def _get_timestamp(item) -> int:
351
- """Extract timestamp from a record item and raise an exception if it is None."""
389
+ """Extract timestamp from a record item and raise an exception if it is None.
390
+ :param item: Record item
391
+ :return: Timestamp
392
+ """
352
393
  timestamp = (
353
394
  item.timestamp
354
395
  or item.completed_at
@@ -375,7 +416,10 @@ class DownloadDecryptMedia:
375
416
  self.client = client
376
417
 
377
418
  async def get_fpath(self, file_name: str) -> Path:
378
- """Return the full path of the file."""
419
+ """Return the full path of the file.
420
+ :param file_name: Name of the file.
421
+ :return: Full path of the file.
422
+ """
379
423
  subdir = ""
380
424
  if file_name.endswith(".jpg"):
381
425
  subdir = "snapshot"
@@ -386,7 +430,10 @@ class DownloadDecryptMedia:
386
430
  async def download_file(
387
431
  self, file_data: MediaCloud, file_type: MediaType | None
388
432
  ) -> None:
389
- """Get image and video file"""
433
+ """Get image and video file
434
+ :param file_data: MediaCloud object
435
+ :param file_type: MediaType object
436
+ """
390
437
  _LOGGER.debug("Downloading media file %s", file_data.event_id)
391
438
  self.file_data = file_data
392
439
 
@@ -453,7 +500,12 @@ class DownloadDecryptMedia:
453
500
  return await self.client.extract_segments_m3u8(str(media_api))
454
501
 
455
502
  async def _get_file(self, url: str, aes_key: str, full_filename: str) -> bool:
456
- """Download a file from a URL and decrypt it."""
503
+ """Download a file from a URL and decrypt it.
504
+ :param url: URL of the file to download.
505
+ :param aes_key: AES key used for decryption.
506
+ :param full_filename: Name of the file to save.
507
+ :return: True if the file was downloaded successfully, False otherwise.
508
+ """
457
509
 
458
510
  full_file_path = await self.get_fpath(full_filename)
459
511
  if full_file_path.exists():
@@ -481,7 +533,11 @@ class DownloadDecryptMedia:
481
533
  return False
482
534
 
483
535
  async def _save_file(self, content: bytes, filename: str) -> Path:
484
- """Save content to a file asynchronously and return the file path."""
536
+ """Save content to a file asynchronously and return the file path.
537
+ :param content: Bytes data to save.
538
+ :param filename: Name of the file to save.
539
+ :return: Path of the saved file.
540
+ """
485
541
  file_path = await self.get_fpath(filename)
486
542
  try:
487
543
  # Ensure the directory exists
@@ -528,11 +584,11 @@ class DownloadDecryptMedia:
528
584
  Path(file_path).unlink()
529
585
  return decrypted_data
530
586
 
531
- async def _concat_segments(self, ts_files: list[Path], output_file):
532
- """Concatenate a list of .ts segments into a single output file without using a temporary file.
587
+ async def _concat_segments(self, ts_files: list[Path], output_file) -> None:
588
+ """Concatenate a list of .avi segments into a single output file without using a temporary file.
533
589
 
534
- :param ts_files: List of absolute paths of .ts files
535
- :param output_file: Path of the output file (e.g., "output.mp4")
590
+ :param ts_files: List of absolute paths of .avi files
591
+ :param output_file: Path of the output file (e.g., "output.avi")
536
592
  """
537
593
  full_output_file = await self.get_fpath(output_file)
538
594
  if full_output_file.exists():
@@ -580,7 +636,9 @@ class DownloadDecryptMedia:
580
636
  _LOGGER.error("OS error during concatenation: %s", e)
581
637
 
582
638
  async def _delete_segments(self, ts_files: list[Path]) -> None:
583
- """Delete all segment files after concatenation."""
639
+ """Delete all segment files after concatenation.
640
+ :param ts_files: List of absolute paths of .avi files
641
+ """
584
642
  for file in ts_files:
585
643
  if file.exists():
586
644
  try:
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "1.10.1"
190
+ version = "1.10.3"
191
191
  description = "Python client for PetKit API"
192
192
  authors = ["Jezza34000 <info@mail.com>"]
193
193
  readme = "README.md"
@@ -209,7 +209,7 @@ ruff = "^0.8.1"
209
209
  types-aiofiles = "^24.1.0.20240626"
210
210
 
211
211
  [tool.bumpver]
212
- current_version = "1.10.1"
212
+ current_version = "1.10.3"
213
213
  version_pattern = "MAJOR.MINOR.PATCH"
214
214
  commit_message = "bump version {old_version} -> {new_version}"
215
215
  tag_message = "{new_version}"
File without changes
File without changes