pyrekordbox 0.4.1__py3-none-any.whl → 0.4.3__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.
pyrekordbox/db6/tables.py CHANGED
@@ -7,15 +7,15 @@
7
7
  import math
8
8
  import re
9
9
  import struct
10
- from datetime import datetime
10
+ from datetime import datetime, timezone
11
11
  from enum import IntEnum
12
- from typing import List
12
+ from typing import Any, Dict, Iterator, List, Optional, Tuple
13
13
 
14
14
  import numpy as np
15
15
  from sqlalchemy import (
16
16
  VARCHAR,
17
17
  BigInteger,
18
- Column,
18
+ Dialect,
19
19
  Float,
20
20
  ForeignKey,
21
21
  Integer,
@@ -71,6 +71,7 @@ __all__ = [
71
71
  "ImageFile",
72
72
  "SettingFile",
73
73
  "UuidIDMap",
74
+ "FileType",
74
75
  ]
75
76
 
76
77
 
@@ -116,13 +117,11 @@ TABLES = [
116
117
 
117
118
 
118
119
  def datetime_to_str(value: datetime) -> str:
119
- s = value.isoformat().replace("T", " ")
120
- if value.tzinfo is not None:
121
- # Get the timezone info (last 6 characters of the string)
122
- tzinfo = s[-6:]
123
- s = s[:-9] + " " + tzinfo
124
- else:
125
- s = s[:-3] + " +00:00"
120
+ # Convert to UTC timezone string
121
+ s = value.astimezone(timezone.utc).isoformat().replace("T", " ")
122
+ # Get the timezone info (last 6 characters of the string)
123
+ tzinfo = s[-6:]
124
+ s = s[:-9] + " " + tzinfo
126
125
  return s
127
126
 
128
127
 
@@ -146,7 +145,7 @@ def string_to_datetime(value: str) -> datetime:
146
145
  return dt.astimezone().replace(tzinfo=None)
147
146
 
148
147
 
149
- class DateTime(TypeDecorator):
148
+ class DateTime(TypeDecorator): # type: ignore[type-arg]
150
149
  """Custom datetime column with timezone support.
151
150
 
152
151
  The datetime format in the database is `YYYY-MM-DD HH:MM:SS.SSS +00:00`.
@@ -156,10 +155,10 @@ class DateTime(TypeDecorator):
156
155
  impl = Text
157
156
  cache_ok = True
158
157
 
159
- def process_bind_param(self, value: datetime, dialect) -> str:
158
+ def process_bind_param(self, value: datetime, dialect: Dialect) -> str: # type: ignore[override]
160
159
  return datetime_to_str(value)
161
160
 
162
- def process_result_value(self, value: str, dialect):
161
+ def process_result_value(self, value: str, dialect: Dialect) -> Optional[datetime]: # type: ignore[override]
163
162
  if value:
164
163
  return string_to_datetime(value)
165
164
  return None
@@ -190,65 +189,65 @@ class Base(DeclarativeBase):
190
189
  __keys__: List[str] = []
191
190
 
192
191
  @classmethod
193
- def create(cls, **kwargs):
192
+ def create(cls, **kwargs: Any): # type: ignore # noqa: ANN206
194
193
  with RekordboxAgentRegistry.disabled():
195
194
  # noinspection PyArgumentList
196
195
  self = cls(**kwargs)
197
196
  return self
198
197
 
199
198
  @classmethod
200
- def columns(cls):
199
+ def columns(cls) -> List[str]:
201
200
  """Returns a list of all column names without the relationships."""
202
201
  return [column.name for column in inspect(cls).c]
203
202
 
204
203
  @classmethod
205
- def relationships(cls):
204
+ def relationships(cls) -> List[str]:
206
205
  """Returns a list of all relationship names."""
207
206
  return [column.key for column in inspect(cls).relationships] # noqa
208
207
 
209
208
  @classmethod
210
- def __get_keys__(cls):
209
+ def __get_keys__(cls) -> List[str]: # pragma: no cover
211
210
  """Get all attributes of the table."""
212
211
  items = cls.__dict__.items()
213
212
  keys = [k for k, v in items if not callable(v) and not k.startswith("_")]
214
213
  return keys
215
214
 
216
215
  @classmethod
217
- def keys(cls):
216
+ def keys(cls) -> List[str]: # pragma: no cover
218
217
  """Returns a list of all column names including the relationships."""
219
218
  if not cls.__keys__: # Cache the keys
220
219
  cls.__keys__ = cls.__get_keys__()
221
220
  return cls.__keys__
222
221
 
223
- def __iter__(self):
222
+ def __iter__(self) -> Iterator[str]:
224
223
  """Iterates over all columns and relationship names."""
225
224
  return iter(self.keys())
226
225
 
227
- def __len__(self):
226
+ def __len__(self) -> int:
228
227
  return sum(1 for _ in self.__iter__())
229
228
 
230
- def __getitem__(self, item):
229
+ def __getitem__(self, item: str) -> Any:
231
230
  return self.__getattribute__(item)
232
231
 
233
232
  # noinspection PyUnresolvedReferences
234
- def __setattr__(self, key, value):
233
+ def __setattr__(self, key: str, value: Any) -> None:
235
234
  if not key.startswith("_"):
236
235
  RekordboxAgentRegistry.on_update(self, key, value)
237
236
  super().__setattr__(key, value)
238
237
 
239
- def values(self):
238
+ def values(self) -> List[Any]:
240
239
  """Returns a list of all column values including the relationships."""
241
240
  return [self.__getitem__(key) for key in self.keys()]
242
241
 
243
- def items(self):
242
+ def items(self) -> Iterator[Tuple[str, Any]]:
244
243
  for key in self.__iter__():
245
244
  yield key, self.__getitem__(key)
246
245
 
247
- def to_dict(self):
246
+ def to_dict(self) -> Dict[str, Any]:
248
247
  """Returns a dictionary of all column names and values."""
249
248
  return {key: self.__getitem__(key) for key in self.columns()}
250
249
 
251
- def pformat(self, indent=" "):
250
+ def pformat(self, indent: str = " ") -> str: # pragma: no cover
252
251
  lines = [f"{self.__tablename__}"]
253
252
  columns = self.columns()
254
253
  w = max(len(col) for col in columns)
@@ -271,7 +270,7 @@ class StatsTime:
271
270
  class StatsFull:
272
271
  """Mixin class for tables that use all statistics columns."""
273
272
 
274
- ID: Column
273
+ ID: Mapped[str]
275
274
  """The ID (primary key) of the table entry."""
276
275
 
277
276
  UUID: Mapped[str] = mapped_column(VARCHAR(255), default=None)
@@ -296,7 +295,7 @@ class StatsFull:
296
295
  )
297
296
  """The last update date of the table entry (from :class:`StatsFull`)."""
298
297
 
299
- def __repr__(self):
298
+ def __repr__(self) -> str:
300
299
  return f"<{self.__class__.__name__}({self.ID})>"
301
300
 
302
301
 
@@ -525,7 +524,7 @@ class DjmdAlbum(Base, StatsFull):
525
524
  AlbumArtistName = association_proxy("AlbumArtist", "Name")
526
525
  """The name of the album artist (:class:`DjmdArtist`) of the track."""
527
526
 
528
- def __repr__(self):
527
+ def __repr__(self) -> str:
529
528
  s = f"{self.ID: <10} Name={self.Name}"
530
529
  return f"<{self.__class__.__name__}({s})>"
531
530
 
@@ -542,7 +541,7 @@ class DjmdArtist(Base, StatsFull):
542
541
  SearchStr: Mapped[str] = mapped_column(VARCHAR(255), default=None)
543
542
  """The search string of the artist."""
544
543
 
545
- def __repr__(self):
544
+ def __repr__(self) -> str:
546
545
  s = f"{self.ID: <10} Name={self.Name}"
547
546
  return f"<{self.__class__.__name__}({s})>"
548
547
 
@@ -588,7 +587,7 @@ class DjmdColor(Base, StatsFull):
588
587
  Commnt: Mapped[str] = mapped_column(VARCHAR(255), default=None)
589
588
  """The comment (name) of the color."""
590
589
 
591
- def __repr__(self):
590
+ def __repr__(self) -> str:
592
591
  s = f"{self.ID: <2} Comment={self.Commnt}"
593
592
  return f"<{self.__class__.__name__}({s})>"
594
593
 
@@ -807,7 +806,7 @@ class DjmdContent(Base, StatsFull):
807
806
  MyTagIDs = association_proxy("MyTags", "MyTagID")
808
807
  """The IDs of the my tags (:class:`DjmdSongMyTag`) of the track."""
809
808
 
810
- def __repr__(self):
809
+ def __repr__(self) -> str:
811
810
  s = f"{self.ID: <10} Title={self.Title}"
812
811
  return f"<{self.__class__.__name__}({s})>"
813
812
 
@@ -869,11 +868,11 @@ class DjmdCue(Base, StatsFull):
869
868
  """The content entry of the cue point (links to :class:`DjmdContent`)."""
870
869
 
871
870
  @property
872
- def is_memory_cue(self):
871
+ def is_memory_cue(self) -> bool:
873
872
  return self.Kind == 0
874
873
 
875
874
  @property
876
- def is_hot_cue(self):
875
+ def is_hot_cue(self) -> bool:
877
876
  return self.Kind > 0
878
877
 
879
878
 
@@ -889,7 +888,7 @@ class DjmdDevice(Base, StatsFull):
889
888
  Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
890
889
  """The name of the device."""
891
890
 
892
- def __repr__(self):
891
+ def __repr__(self) -> str:
893
892
  s = f"{self.ID: <2} Name={self.Name}"
894
893
  return f"<{self.__class__.__name__}({s})>"
895
894
 
@@ -904,7 +903,7 @@ class DjmdGenre(Base, StatsFull):
904
903
  Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
905
904
  """The name of the genre."""
906
905
 
907
- def __repr__(self):
906
+ def __repr__(self) -> str:
908
907
  s = f"{self.ID: <2} Name={self.Name}"
909
908
  return f"<{self.__class__.__name__}({s})>"
910
909
 
@@ -943,7 +942,7 @@ class DjmdHistory(Base, StatsFull):
943
942
  Backrefs to the parent history playlist via :attr:`Parent`.
944
943
  """
945
944
 
946
- def __repr__(self):
945
+ def __repr__(self) -> str:
947
946
  s = f"{self.ID: <2} Name={self.Name}"
948
947
  return f"<{self.__class__.__name__}({s})>"
949
948
 
@@ -1007,7 +1006,7 @@ class DjmdHotCueBanklist(Base, StatsFull):
1007
1006
  Backrefs to the parent hot-cue banklist via :attr:`Parent`.
1008
1007
  """
1009
1008
 
1010
- def __repr__(self):
1009
+ def __repr__(self) -> str:
1011
1010
  s = f"{self.ID: <2} Name={self.Name}"
1012
1011
  return f"<{self.__class__.__name__}({s})>"
1013
1012
 
@@ -1087,7 +1086,7 @@ class DjmdKey(Base, StatsFull):
1087
1086
  Seq: Mapped[int] = mapped_column(Integer, default=None)
1088
1087
  """The sequence of the key (for ordering)."""
1089
1088
 
1090
- def __repr__(self):
1089
+ def __repr__(self) -> str:
1091
1090
  s = f"{self.ID: <2} Name={self.ScaleName}"
1092
1091
  return f"<{self.__class__.__name__}({s})>"
1093
1092
 
@@ -1102,7 +1101,7 @@ class DjmdLabel(Base, StatsFull):
1102
1101
  Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
1103
1102
  """The name of the label."""
1104
1103
 
1105
- def __repr__(self):
1104
+ def __repr__(self) -> str:
1106
1105
  s = f"{self.ID: <2} Name={self.Name}"
1107
1106
  return f"<{self.__class__.__name__}({s})>"
1108
1107
 
@@ -1119,7 +1118,7 @@ class DjmdMenuItems(Base, StatsFull):
1119
1118
  Name: Mapped[str] = mapped_column(VARCHAR(255), default=None)
1120
1119
  """The name of the menu item."""
1121
1120
 
1122
- def __repr__(self):
1121
+ def __repr__(self) -> str:
1123
1122
  s = f"{self.ID: <2} Name={self.Name}"
1124
1123
  return f"<{self.__class__.__name__}({s})>"
1125
1124
 
@@ -1151,7 +1150,7 @@ class DjmdMixerParam(Base, StatsFull):
1151
1150
  """The content this mixer parameters belong to (links to :class:`DjmdContent`)."""
1152
1151
 
1153
1152
  @staticmethod
1154
- def _get_db(low, high):
1153
+ def _get_db(low: int, high: int) -> float:
1155
1154
  integer = (high << 16) | low
1156
1155
  byte_data = integer.to_bytes(4, byteorder="big")
1157
1156
  factor = struct.unpack("!f", byte_data)[0]
@@ -1160,7 +1159,7 @@ class DjmdMixerParam(Base, StatsFull):
1160
1159
  return 20 * math.log10(factor)
1161
1160
 
1162
1161
  @staticmethod
1163
- def _set_db(value):
1162
+ def _set_db(value: float) -> Tuple[int, int]:
1164
1163
  if value == -np.inf:
1165
1164
  return 0, 0
1166
1165
  factor = 10 ** (value / 20)
@@ -1184,7 +1183,7 @@ class DjmdMixerParam(Base, StatsFull):
1184
1183
  self.GainLow, self.GainHigh = self._set_db(value)
1185
1184
 
1186
1185
  @property
1187
- def Peak(self):
1186
+ def Peak(self) -> float:
1188
1187
  """The peak amplitude of a track in dB.
1189
1188
 
1190
1189
  This value is computed from the low and high peak values.
@@ -1193,7 +1192,7 @@ class DjmdMixerParam(Base, StatsFull):
1193
1192
  return self._get_db(self.PeakLow, self.PeakHigh)
1194
1193
 
1195
1194
  @Peak.setter
1196
- def Peak(self, value):
1195
+ def Peak(self, value: float) -> None:
1197
1196
  self.PeakLow, self.PeakHigh = self._set_db(value)
1198
1197
 
1199
1198
 
@@ -1229,7 +1228,7 @@ class DjmdMyTag(Base, StatsFull):
1229
1228
  Backrefs to the parent list via :attr:`Parent`.
1230
1229
  """
1231
1230
 
1232
- def __repr__(self):
1231
+ def __repr__(self) -> str:
1233
1232
  s = f"{self.ID: <2} Name={self.Name}"
1234
1233
  return f"<{self.__class__.__name__}({s})>"
1235
1234
 
@@ -1300,18 +1299,18 @@ class DjmdPlaylist(Base, StatsFull):
1300
1299
  """
1301
1300
 
1302
1301
  @property
1303
- def is_playlist(self):
1302
+ def is_playlist(self) -> bool:
1304
1303
  return self.Attribute == PlaylistType.PLAYLIST
1305
1304
 
1306
1305
  @property
1307
- def is_folder(self):
1306
+ def is_folder(self) -> bool:
1308
1307
  return self.Attribute == PlaylistType.FOLDER
1309
1308
 
1310
1309
  @property
1311
- def is_smart_playlist(self):
1310
+ def is_smart_playlist(self) -> bool:
1312
1311
  return self.Attribute == PlaylistType.SMART_PLAYLIST
1313
1312
 
1314
- def __repr__(self):
1313
+ def __repr__(self) -> str:
1315
1314
  s = f"{self.ID: <2} Name={self.Name}"
1316
1315
  return f"<{self.__class__.__name__}({s})>"
1317
1316
 
@@ -1381,7 +1380,7 @@ class DjmdRelatedTracks(Base, StatsFull):
1381
1380
  Backrefs to the parent related tracks list via :attr:`Parent`.
1382
1381
  """
1383
1382
 
1384
- def __repr__(self):
1383
+ def __repr__(self) -> str:
1385
1384
  s = f"{self.ID: <2} Name={self.Name}"
1386
1385
  return f"<{self.__class__.__name__}({s})>"
1387
1386
 
@@ -1446,7 +1445,7 @@ class DjmdSampler(Base, StatsFull):
1446
1445
  Backrefs to the parent sampler list via :attr:`Parent`.
1447
1446
  """
1448
1447
 
1449
- def __repr__(self):
1448
+ def __repr__(self) -> str:
1450
1449
  s = f"{self.ID: <2} Name={self.Name}"
1451
1450
  return f"<{self.__class__.__name__}({s})>"
1452
1451
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  import re
6
6
  from pathlib import Path
7
+ from typing import List, Union
7
8
 
8
9
  from . import structs
9
10
  from .file import (
@@ -18,7 +19,7 @@ from .file import (
18
19
  RE_MYSETTING = re.compile(".*SETTING[0-9]?.DAT$")
19
20
 
20
21
 
21
- def get_mysetting_paths(root, deep=False):
22
+ def get_mysetting_paths(root: Union[str, Path], deep: bool = False) -> List[Path]:
22
23
  files = list()
23
24
  root = Path(root)
24
25
  iteator = root.rglob("*") if deep else root.iterdir()
@@ -28,6 +29,6 @@ def get_mysetting_paths(root, deep=False):
28
29
  return files
29
30
 
30
31
 
31
- def read_mysetting_file(path) -> SettingsFile:
32
+ def read_mysetting_file(path: Union[str, Path]) -> SettingsFile:
32
33
  obj = FILES[str(Path(path).name)]
33
34
  return obj.parse_file(path)
@@ -6,6 +6,8 @@
6
6
 
7
7
  import re
8
8
  from collections.abc import MutableMapping
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Iterator, Type, Union
9
11
 
10
12
  from construct import Struct
11
13
 
@@ -51,7 +53,7 @@ CRC16_XMODEM_TABLE = [
51
53
  RE_INVALID_KEY = re.compile("[_u][0-9]?", flags=re.IGNORECASE)
52
54
 
53
55
 
54
- def compute_checksum(data, struct):
56
+ def compute_checksum(data: bytes, struct: Struct) -> int:
55
57
  """Computes the CRC16 XModem checksum for My-Setting files.
56
58
 
57
59
  The checksum is calculated over the contents of the `data` field,
@@ -83,11 +85,11 @@ def compute_checksum(data, struct):
83
85
  return crc
84
86
 
85
87
 
86
- def _is_valid_key(k: str):
88
+ def _is_valid_key(k: str) -> bool:
87
89
  return not RE_INVALID_KEY.match(k)
88
90
 
89
91
 
90
- class SettingsFile(MutableMapping):
92
+ class SettingsFile(MutableMapping): # type: ignore[type-arg]
91
93
  """Base class for the Rekordbox My-Setting file handler.
92
94
 
93
95
  The base class implements the getters and setter defined by the keys and
@@ -98,16 +100,16 @@ class SettingsFile(MutableMapping):
98
100
  """
99
101
 
100
102
  struct: Struct
101
- defaults: dict
103
+ defaults: Dict[str, str]
102
104
  version: str = "" # only used by DEVSETTING
103
105
 
104
- def __init__(self):
106
+ def __init__(self) -> None:
105
107
  super().__init__()
106
108
  self.parsed = None
107
- self._items = dict()
109
+ self._items: Dict[str, str] = dict()
108
110
 
109
111
  @classmethod
110
- def parse(cls, data: bytes):
112
+ def parse(cls, data: bytes) -> "SettingsFile":
111
113
  """Parses the in-memory data of a Rekordbox settings binary file.
112
114
 
113
115
  Parameters
@@ -125,18 +127,18 @@ class SettingsFile(MutableMapping):
125
127
  return self
126
128
 
127
129
  @classmethod
128
- def parse_file(cls, path: str):
130
+ def parse_file(cls, path: Union[str, Path]) -> "SettingsFile":
129
131
  """Reads and parses a Rekordbox settings binary file.
130
132
 
131
133
  Parameters
132
134
  ----------
133
- path : str
135
+ path : str or Path
134
136
  The path of a Rekordbox settings file which is used to read
135
137
  the file contents before parsing the binary data.
136
138
 
137
139
  Returns
138
140
  -------
139
- self : AnlzFile
141
+ self : SettingsFile
140
142
  The new instance with the parsed file content.
141
143
 
142
144
  See Also
@@ -147,7 +149,7 @@ class SettingsFile(MutableMapping):
147
149
  data = fh.read()
148
150
  return cls.parse(data)
149
151
 
150
- def _parse(self, data):
152
+ def _parse(self, data: bytes) -> None:
151
153
  parsed = self.struct.parse(data)
152
154
  keys = filter(_is_valid_key, parsed.data.keys())
153
155
  items = dict()
@@ -157,27 +159,27 @@ class SettingsFile(MutableMapping):
157
159
  self.parsed = parsed
158
160
  self._items.update(items)
159
161
 
160
- def __len__(self):
162
+ def __len__(self) -> int:
161
163
  return len(self.defaults.keys())
162
164
 
163
- def __iter__(self):
165
+ def __iter__(self) -> Iterator[str]:
164
166
  return iter(self.defaults)
165
167
 
166
- def __getitem__(self, key):
168
+ def __getitem__(self, key: str) -> str:
167
169
  try:
168
170
  return self._items[key]
169
171
  except KeyError:
170
172
  return self.defaults[key]
171
173
 
172
- def __setitem__(self, key, value):
174
+ def __setitem__(self, key: str, value: str) -> None:
173
175
  if key not in self.defaults.keys():
174
176
  raise KeyError(f"Key {key} not a valid field of {self.__class__.__name__}")
175
177
  self._items[key] = value
176
178
 
177
- def __delitem__(self, key):
179
+ def __delitem__(self, key: str) -> None:
178
180
  del self._items[key]
179
181
 
180
- def get(self, key, default=None):
182
+ def get(self, key: str, default: str = None) -> Union[str, None]: # type: ignore[override]
181
183
  """Returns the value of a setting of the My-Setting file.
182
184
 
183
185
  If the key is not found in the My-Setting data, but it is present in the
@@ -202,7 +204,7 @@ class SettingsFile(MutableMapping):
202
204
  except KeyError:
203
205
  return default
204
206
 
205
- def set(self, key, value):
207
+ def set(self, key: str, value: str) -> None:
206
208
  """Sets the value of a setting of the My-Setting file.
207
209
 
208
210
  Parameters
@@ -214,7 +216,7 @@ class SettingsFile(MutableMapping):
214
216
  """
215
217
  self.__setitem__(key, value)
216
218
 
217
- def build(self):
219
+ def build(self) -> bytes:
218
220
  """Constructs the binary data for saving the My-Setting file.
219
221
 
220
222
  Returns
@@ -227,7 +229,7 @@ class SettingsFile(MutableMapping):
227
229
  items.update(self._items)
228
230
 
229
231
  # Create file data
230
- file_items = {"data": items, "checksum": 0}
232
+ file_items: Dict[str, Any] = {"data": items, "checksum": 0}
231
233
  if self.version:
232
234
  file_items["version"] = self.version
233
235
 
@@ -239,9 +241,10 @@ class SettingsFile(MutableMapping):
239
241
  file_items["checksum"] = checksum
240
242
 
241
243
  # Write data with updated checksum
242
- return self.struct.build(file_items)
244
+ bytedata: bytes = self.struct.build(file_items)
245
+ return bytedata
243
246
 
244
- def save(self, path):
247
+ def save(self, path: Union[str, Path]) -> None:
245
248
  """Save the contents of the My-Setting file object.
246
249
 
247
250
  Parameters
@@ -257,7 +260,7 @@ class SettingsFile(MutableMapping):
257
260
  with open(path, "wb") as fh:
258
261
  fh.write(data)
259
262
 
260
- def __repr__(self):
263
+ def __repr__(self) -> str:
261
264
  return f"{self.__class__.__name__}()"
262
265
 
263
266
 
@@ -363,7 +366,7 @@ class DevSettingFile(SettingsFile):
363
366
  defaults = dict(entries="")
364
367
 
365
368
 
366
- FILES = {
369
+ FILES: Dict[str, Type[SettingsFile]] = {
367
370
  "DEVSETTING.DAT": DevSettingFile,
368
371
  "DJMMYSETTING.DAT": DjmMySettingFile,
369
372
  "MYSETTING.DAT": MySettingFile,