pyrekordbox 0.2.0__py3-none-any.whl → 0.2.2__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.
Files changed (69) hide show
  1. docs/Makefile +20 -0
  2. docs/make.bat +35 -0
  3. docs/source/_static/images/anlz_beat.svg +53 -0
  4. docs/source/_static/images/anlz_file.svg +204 -0
  5. docs/source/_static/images/anlz_pco2.svg +138 -0
  6. docs/source/_static/images/anlz_pcob.svg +148 -0
  7. docs/source/_static/images/anlz_pcp2.svg +398 -0
  8. docs/source/_static/images/anlz_pcpt.svg +263 -0
  9. docs/source/_static/images/anlz_ppth.svg +123 -0
  10. docs/source/_static/images/anlz_pqt2.svg +324 -0
  11. docs/source/_static/images/anlz_pqt2_2.svg +253 -0
  12. docs/source/_static/images/anlz_pqtz.svg +140 -0
  13. docs/source/_static/images/anlz_pssi.svg +192 -0
  14. docs/source/_static/images/anlz_pssi_entry.svg +191 -0
  15. docs/source/_static/images/anlz_pvbr.svg +125 -0
  16. docs/source/_static/images/anlz_pwav.svg +130 -0
  17. docs/source/_static/images/anlz_pwv3.svg +139 -0
  18. docs/source/_static/images/anlz_pwv4.svg +139 -0
  19. docs/source/_static/images/anlz_pwv5.svg +139 -0
  20. docs/source/_static/images/anlz_pwv5_entry.svg +100 -0
  21. docs/source/_static/images/anlz_pwv6.svg +130 -0
  22. docs/source/_static/images/anlz_pwv7.svg +139 -0
  23. docs/source/_static/images/anlz_pwvc.svg +125 -0
  24. docs/source/_static/images/anlz_tag.svg +110 -0
  25. docs/source/_static/logos/dark/logo_primary.svg +75 -0
  26. docs/source/_static/logos/light/logo_primary.svg +75 -0
  27. docs/source/_static/logos/mid/logo_primary.svg +75 -0
  28. docs/source/_templates/apidoc/module.rst_t +8 -0
  29. docs/source/_templates/apidoc/package.rst_t +57 -0
  30. docs/source/_templates/apidoc/toc.rst_t +7 -0
  31. docs/source/_templates/autosummary/class.rst +32 -0
  32. docs/source/_templates/autosummary/module.rst +55 -0
  33. docs/source/api.md +18 -0
  34. docs/source/conf.py +178 -0
  35. docs/source/development/changes.md +3 -0
  36. docs/source/development/contributing.md +3 -0
  37. docs/source/formats/anlz.md +634 -0
  38. docs/source/formats/db6.md +1233 -0
  39. docs/source/formats/mysetting.md +392 -0
  40. docs/source/formats/xml.md +376 -0
  41. docs/source/index.md +105 -0
  42. docs/source/installation.md +3 -0
  43. docs/source/quickstart.md +185 -0
  44. docs/source/requirements.txt +7 -0
  45. docs/source/tutorial/anlz.md +7 -0
  46. docs/source/tutorial/configuration.md +66 -0
  47. docs/source/tutorial/db6.md +179 -0
  48. docs/source/tutorial/index.md +20 -0
  49. docs/source/tutorial/mysetting.md +124 -0
  50. docs/source/tutorial/xml.md +140 -0
  51. pyrekordbox/__init__.py +1 -1
  52. pyrekordbox/__main__.py +16 -37
  53. pyrekordbox/_version.py +2 -2
  54. pyrekordbox/anlz/file.py +39 -0
  55. pyrekordbox/anlz/structs.py +3 -5
  56. pyrekordbox/config.py +71 -27
  57. pyrekordbox/db6/database.py +290 -61
  58. pyrekordbox/db6/registry.py +24 -0
  59. pyrekordbox/db6/tables.py +501 -340
  60. pyrekordbox/mysettings/file.py +0 -25
  61. pyrekordbox/utils.py +1 -1
  62. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/METADATA +42 -20
  63. pyrekordbox-0.2.2.dist-info/RECORD +80 -0
  64. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/top_level.txt +1 -0
  65. tests/test_config.py +175 -0
  66. tests/test_db6.py +95 -0
  67. pyrekordbox-0.2.0.dist-info/RECORD +0 -29
  68. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/LICENSE +0 -0
  69. {pyrekordbox-0.2.0.dist-info → pyrekordbox-0.2.2.dist-info}/WHEEL +0 -0
@@ -9,9 +9,10 @@ from uuid import uuid4
9
9
  from pathlib import Path
10
10
  from typing import Optional
11
11
  from sqlalchemy import create_engine, or_, event, MetaData
12
- from sqlalchemy.orm import sessionmaker, Session
12
+ from sqlalchemy.orm import Session
13
13
  from sqlalchemy.exc import NoResultFound
14
- from packaging import version
14
+ from sqlalchemy.sql.sqltypes import DateTime, String
15
+ import packaging.version
15
16
  from ..utils import get_rekordbox_pid
16
17
  from ..config import get_config
17
18
  from ..anlz import get_anlz_paths, read_anlz_files
@@ -21,14 +22,11 @@ from .tables import DjmdContent
21
22
  from . import tables
22
23
 
23
24
  try:
24
- from pysqlcipher3 import dbapi2 as sqlite3 # noqa
25
+ from sqlcipher3 import dbapi2 as sqlite3 # noqa
25
26
  except ImportError:
26
- try:
27
- from sqlcipher3 import dbapi2 as sqlite3 # noqa
28
- except ImportError:
29
- import sqlite3
27
+ import sqlite3
30
28
 
31
- MAX_VERSION = version.parse("6.6.5")
29
+ MAX_VERSION = packaging.version.parse("6.6.5")
32
30
 
33
31
  logger = logging.getLogger(__name__)
34
32
 
@@ -87,12 +85,12 @@ def open_rekordbox_database(path=None, key="", unlock=True, sql_driver=None):
87
85
 
88
86
  To use the ``pysqlcipher3`` package as SQLite driver, either import it as
89
87
 
90
- >>> from pysqlcipher3 import dbapi2 as sqlite3 # noqa
88
+ >>> from sqlcipher3 import dbapi2 as sqlite3 # noqa
91
89
  >>> db = open_rekordbox_database("path/to/master_copy.db")
92
90
 
93
91
  or supply the package as driver:
94
92
 
95
- >>> from pysqlcipher3 import dbapi2
93
+ >>> from sqlcipher3 import dbapi2 # noqa
96
94
  >>> db = open_rekordbox_database("path/to/master_copy.db", sql_driver=dbapi2)
97
95
  """
98
96
  if not path:
@@ -111,7 +109,7 @@ def open_rekordbox_database(path=None, key="", unlock=True, sql_driver=None):
111
109
 
112
110
  if unlock:
113
111
  if not key:
114
- ver = version.parse(rb6_config["version"])
112
+ ver = packaging.version.parse(rb6_config["version"])
115
113
  if ver >= MAX_VERSION:
116
114
  raise IncompatibleVersionError(rb6_config["version"])
117
115
  try:
@@ -209,15 +207,15 @@ class Rekordbox6Database:
209
207
  raise FileNotFoundError(f"File '{path}' does not exist!")
210
208
  # Open database
211
209
  if unlock:
210
+ if "dp" in rb6_config:
211
+ key = rb6_config["dp"]
212
212
  if not key:
213
- ver = version.parse(rb6_config["version"])
213
+ ver = packaging.version.parse(rb6_config["version"])
214
214
  if ver >= MAX_VERSION:
215
215
  raise IncompatibleVersionError(rb6_config["version"])
216
- try:
217
- key = rb6_config["dp"]
218
- except KeyError:
216
+ else:
219
217
  raise ValueError("Could not unlock database: No key found")
220
- logger.info("Key: %s", key)
218
+ logger.info("Key: %s", key)
221
219
  # Unlock database and create engine
222
220
  url = f"sqlite+pysqlcipher://:{key}@/{path}?"
223
221
  engine = create_engine(url, module=sqlite3)
@@ -231,7 +229,6 @@ class Rekordbox6Database:
231
229
  raise FileNotFoundError(f"Database directory '{db_dir}' does not exist!")
232
230
 
233
231
  self.engine = engine
234
- self._Session = sessionmaker(bind=self.engine)
235
232
  self.session: Optional[Session] = None
236
233
 
237
234
  self.registry = RekordboxAgentRegistry(self)
@@ -272,7 +269,7 @@ class Rekordbox6Database:
272
269
  >>> db.open()
273
270
  """
274
271
  if self.session is None:
275
- self.session = self._Session()
272
+ self.session = Session(bind=self.engine)
276
273
  self.registry.clear_buffer()
277
274
 
278
275
  def close(self):
@@ -928,12 +925,12 @@ class Rekordbox6Database:
928
925
  .order_by(tables.DjmdSongPlaylist.TrackNo)
929
926
  )
930
927
  moved = list()
931
- self.registry.disable_tracking()
932
- for song in query:
933
- song.TrackNo -= 1
934
- song.updated_at = now
935
- moved.append(song)
936
- self.registry.enable_tracking()
928
+ with self.registry.disabled():
929
+ for song in query:
930
+ song.TrackNo -= 1
931
+ song.updated_at = now
932
+ moved.append(song)
933
+
937
934
  if moved:
938
935
  self.registry.on_move(moved)
939
936
 
@@ -1095,9 +1092,8 @@ class Rekordbox6Database:
1095
1092
  )
1096
1093
  for pl in query:
1097
1094
  pl.Seq += 1
1098
- self.registry.disable_tracking()
1099
- pl.updated_at = now
1100
- self.registry.enable_tracking()
1095
+ with self.registry.disabled():
1096
+ pl.updated_at = now
1101
1097
 
1102
1098
  # Add new playlist to database
1103
1099
  # First create with name 'New playlist'
@@ -1393,10 +1389,9 @@ class Rekordbox6Database:
1393
1389
  # Set seq number and update time *before* other playlists to ensure
1394
1390
  # right USN increment order
1395
1391
  playlist.ParentID = parent_id
1396
- self.registry.disable_tracking()
1397
- playlist.Seq = seq
1398
- playlist.updated_at = now
1399
- self.registry.enable_tracking()
1392
+ with self.registry.disabled():
1393
+ playlist.Seq = seq
1394
+ playlist.updated_at = now
1400
1395
 
1401
1396
  if not insert_at_end:
1402
1397
  # Update seq numbers higher than the new seq number in *new* parent
@@ -1405,9 +1400,8 @@ class Rekordbox6Database:
1405
1400
  # Update time of other playlists are left unchanged
1406
1401
  pl.Seq += 1
1407
1402
  # Each move counts as one USN increment, so disable for update time
1408
- self.registry.disable_tracking()
1409
- pl.updated_at = now
1410
- self.registry.enable_tracking()
1403
+ with self.registry.disabled():
1404
+ pl.updated_at = now
1411
1405
 
1412
1406
  # Update seq numbers higher than the old seq number in *old* parent
1413
1407
  # USN is not updated here
@@ -1466,17 +1460,15 @@ class Rekordbox6Database:
1466
1460
  # right USN increment order
1467
1461
  playlist.Seq = seq
1468
1462
  # Each move counts as one USN increment, so disable for update time
1469
- self.registry.disable_tracking()
1470
- playlist.updated_at = now
1471
- self.registry.enable_tracking()
1463
+ with self.registry.disabled():
1464
+ playlist.updated_at = now
1472
1465
 
1473
1466
  # Set seq number and update time for playlists between old_seq and seq
1474
1467
  for pl in other_playlists:
1475
1468
  pl.Seq += delta_seq
1476
1469
  # Each move counts as one USN increment, so disable for update time
1477
- self.registry.disable_tracking()
1478
- pl.updated_at = now
1479
- self.registry.enable_tracking()
1470
+ with self.registry.disabled():
1471
+ pl.updated_at = now
1480
1472
 
1481
1473
  def rename_playlist(self, playlist, name):
1482
1474
  """Renames a playlist or playlist folder.
@@ -1512,9 +1504,241 @@ class Rekordbox6Database:
1512
1504
  # Update name of playlist
1513
1505
  playlist.Name = name
1514
1506
  # Update update time: USN not incremented
1515
- self.registry.disable_tracking()
1516
- playlist.updated_at = now
1517
- self.registry.enable_tracking()
1507
+ with self.registry.disabled():
1508
+ playlist.updated_at = now
1509
+
1510
+ def add_album(
1511
+ self, name, artist=None, image_path=None, compilation=None, search_str=None
1512
+ ):
1513
+ """Adds a new album to the database.
1514
+
1515
+ Parameters
1516
+ ----------
1517
+ name : str
1518
+ The name of the album. Must be a unique name (case-sensitive).
1519
+ If an album with the same name already exists in the database,
1520
+ use the `ID` of the existing album instead.
1521
+ artist : str or int or DjmdArtist, optional
1522
+ The artist of the album. Can either be a :class:`DjmdArtist` object
1523
+ or an artist ID.
1524
+ image_path : str, optional
1525
+ The path to the album cover image.
1526
+ compilation : bool, optional
1527
+ Whether the album is a compilation album. If not given, the
1528
+ default value of `False` is used.
1529
+ search_str : str, optional
1530
+ The search string of the album.
1531
+
1532
+ Returns
1533
+ -------
1534
+ album : DjmdAlbum
1535
+ The newly created album.
1536
+
1537
+ Raises
1538
+ ------
1539
+ ValueError : If an album with the same name already exists in the database.
1540
+
1541
+ Examples
1542
+ --------
1543
+ Add a new album to the database:
1544
+
1545
+ >>> db = Rekordbox6Database()
1546
+ >>> db.add_album(name="Album 1")
1547
+ <DjmdAlbum(148754249 Name=Album 1)>
1548
+
1549
+ Add a new album to the database with an album artist:
1550
+
1551
+ >>> artist = db.get_artist(Name="Artist 1").one() # noqa
1552
+ >>> db.add_album(name="Album 2", artist=artist)
1553
+ <DjmdAlbum(148754249 Name=Album 2)>
1554
+
1555
+ For setting the album of a track, the usual procedure is to first
1556
+ check if an entry with the same album name already exists in the database,
1557
+ and if not, add a new album:
1558
+
1559
+ >>> name = "Album name"
1560
+ >>> content = db.get_content().one()
1561
+ >>> album = db.get_album(Name=name).one_or_none()
1562
+ >>> if album is None:
1563
+ ... album = db.add_album(name=name)
1564
+ >>> content.AlbumID = album.ID
1565
+ """
1566
+ # Check if album already exists
1567
+ query = self.query(tables.DjmdAlbum).filter_by(Name=name)
1568
+ if query.count() > 0:
1569
+ raise ValueError(f"Album '{name}' already exists in database")
1570
+
1571
+ # Get artist ID
1572
+ if artist is not None:
1573
+ if isinstance(artist, (int, str)):
1574
+ artist = self.get_artist(ID=artist)
1575
+ artist = artist.ID
1576
+
1577
+ album = tables.DjmdAlbum.create(
1578
+ ID=self.generate_unused_id(tables.DjmdAlbum),
1579
+ Name=name,
1580
+ AlbumArtistID=artist,
1581
+ ImagePath=image_path,
1582
+ Compilation=compilation,
1583
+ SearchStr=search_str,
1584
+ )
1585
+ self.add(album)
1586
+ self.flush()
1587
+ return album
1588
+
1589
+ def add_artist(self, name, search_str=None):
1590
+ """Adds a new artist to the database.
1591
+
1592
+ Parameters
1593
+ ----------
1594
+ name : str
1595
+ The name of the artist. Must be a unique name (case-sensitive).
1596
+ If an artist with the same name already exists in the database,
1597
+ use the `ID` of the existing artist instead.
1598
+ search_str : str, optional
1599
+ The search string of the artist.
1600
+
1601
+ Returns
1602
+ -------
1603
+ artist : DjmdArtist
1604
+ The newly created artist.
1605
+
1606
+ Raises
1607
+ ------
1608
+ ValueError : If an artist with the same name already exists in the database.
1609
+
1610
+ Examples
1611
+ --------
1612
+ Add a new artist to the database:
1613
+
1614
+ >>> db = Rekordbox6Database()
1615
+ >>> db.add_artist(name="Artist 1")
1616
+ <DjmdArtist(123456789, Name='Artist 1')>
1617
+
1618
+ Add a new artist to the database with a custom search string:
1619
+
1620
+ >>> db.add_artist(name="Artist 2", search_str="artist 2")
1621
+ <DjmdArtist(123456789, Name='Artist 2')>
1622
+
1623
+ For setting the artist of a track, the usual procedure is to first
1624
+ check if an entry with the same artist name already exists in the database,
1625
+ and if not, add a new artist:
1626
+
1627
+ >>> name = "Artist name"
1628
+ >>> content = db.get_content().one()
1629
+ >>> artist = db.get_artist(Name=name).one_or_none()
1630
+ >>> if artist is None:
1631
+ ... artist = db.add_artist(name=name)
1632
+ >>> content.ArtistID = artist.ID
1633
+ """
1634
+ # Check if artist already exists
1635
+ query = self.query(tables.DjmdArtist).filter_by(Name=name)
1636
+ if query.count() > 0:
1637
+ raise ValueError(f"Artist '{name}' already exists in database")
1638
+
1639
+ id_ = self.generate_unused_id(tables.DjmdArtist)
1640
+ artist = tables.DjmdArtist.create(ID=id_, Name=name, SearchStr=search_str)
1641
+ self.add(artist)
1642
+ self.flush()
1643
+ return artist
1644
+
1645
+ def add_genre(self, name):
1646
+ """Adds a new genre to the database.
1647
+
1648
+ Parameters
1649
+ ----------
1650
+ name : str
1651
+ The name of the genre. Must be a unique name (case-sensitive).
1652
+ If a genre with the same name already exists in the database,
1653
+ use the `ID` of the existing genre instead.
1654
+
1655
+ Returns
1656
+ -------
1657
+ genre : DjmdGenre
1658
+ The newly created genre.
1659
+
1660
+ Raises
1661
+ ------
1662
+ ValueError : If a genre with the same name already exists in the database.
1663
+
1664
+ Examples
1665
+ --------
1666
+ Add a new genre to the database:
1667
+
1668
+ >>> db = Rekordbox6Database()
1669
+ >>> db.add_genre(name="Genre 1")
1670
+ <DjmdGenre(123456789 Name=Genre 1)>
1671
+
1672
+ For setting the genre of a track, the usual procedure is to first
1673
+ check if an entry with the same genre name already exists in the database,
1674
+ and if not, add a new genre:
1675
+
1676
+ >>> name = "Genre name"
1677
+ >>> content = db.get_content().one()
1678
+ >>> genre = db.get_genre(Name=name).one_or_none()
1679
+ >>> if genre is None:
1680
+ ... genre = db.add_genre(name=name)
1681
+ >>> content.GenreID = genre.ID
1682
+ """
1683
+ # Check if genre already exists
1684
+ query = self.query(tables.DjmdGenre).filter_by(Name=name)
1685
+ if query.count() > 0:
1686
+ raise ValueError(f"Genre '{name}' already exists in database")
1687
+
1688
+ id_ = self.generate_unused_id(tables.DjmdGenre)
1689
+ genre = tables.DjmdGenre.create(ID=id_, Name=name)
1690
+ self.add(genre)
1691
+ self.flush()
1692
+ return genre
1693
+
1694
+ def add_label(self, name):
1695
+ """Adds a new label to the database.
1696
+
1697
+ Parameters
1698
+ ----------
1699
+ name : str
1700
+ The name of the label. Must be a unique name (case-sensitive).
1701
+ If a label with the same name already exists in the database,
1702
+ use the `ID` of the existing label instead.
1703
+
1704
+ Returns
1705
+ -------
1706
+ label : DjmdLabel
1707
+ The newly created label.
1708
+
1709
+ Raises
1710
+ ------
1711
+ ValueError : If a label with the same name already exists in the database.
1712
+
1713
+ Examples
1714
+ --------
1715
+ Add a new label to the database:
1716
+
1717
+ >>> db = Rekordbox6Database()
1718
+ >>> db.add_label(name="Label 1")
1719
+ <DjmdLabel(123456789 Name=Label 1)>
1720
+
1721
+ For setting the label of a track, the usual procedure is to first
1722
+ check if an entry with the same label name already exists in the database,
1723
+ and if not, add a new label:
1724
+
1725
+ >>> name = "Label name"
1726
+ >>> content = db.get_content().one()
1727
+ >>> label = db.get_label(Name=name).one_or_none()
1728
+ >>> if label is None:
1729
+ ... label = db.add_label(name=name)
1730
+ >>> content.LabelID = label.ID
1731
+ """
1732
+ # Check if label already exists
1733
+ query = self.query(tables.DjmdLabel).filter_by(Name=name)
1734
+ if query.count() > 0:
1735
+ raise ValueError(f"Label '{name}' already exists in database")
1736
+
1737
+ id_ = self.generate_unused_id(tables.DjmdLabel)
1738
+ label = tables.DjmdLabel.create(ID=id_, Name=name)
1739
+ self.add(label)
1740
+ self.flush()
1741
+ return label
1518
1742
 
1519
1743
  # ----------------------------------------------------------------------------------
1520
1744
 
@@ -1761,46 +1985,51 @@ class Rekordbox6Database:
1761
1985
  json.dump(data, fp, indent=indent, sort_keys=sort_keys, default=json_serial)
1762
1986
 
1763
1987
  def copy_unlocked(self, output_file):
1764
- src_metadata = MetaData(bind=self.engine)
1988
+ src_engine = self.engine
1989
+ src_metadata = MetaData()
1765
1990
  exclude_tables = ("sqlite_master", "sqlite_sequence", "sqlite_temp_master")
1766
1991
 
1767
1992
  dst_engine = create_engine(f"sqlite:///{output_file}")
1768
- dst_metadata = MetaData(bind=dst_engine)
1993
+ dst_metadata = MetaData()
1769
1994
 
1770
1995
  @event.listens_for(src_metadata, "column_reflect")
1771
1996
  def genericize_datatypes(inspector, tablename, column_dict):
1772
- column_dict["type"] = column_dict["type"].as_generic(allow_nulltype=True)
1773
-
1774
- dst_engine.connect()
1775
- dst_metadata.reflect()
1776
-
1997
+ type_ = column_dict["type"].as_generic(allow_nulltype=True)
1998
+ if isinstance(type_, DateTime):
1999
+ type_ = String
2000
+ column_dict["type"] = type_
2001
+
2002
+ src_conn = src_engine.connect()
2003
+ dst_conn = dst_engine.connect()
2004
+ dst_metadata.reflect(bind=dst_engine)
1777
2005
  # drop all tables in target database
1778
2006
  for table in reversed(dst_metadata.sorted_tables):
1779
2007
  if table.name not in exclude_tables:
1780
2008
  print("dropping table =", table.name)
1781
- table.drop()
1782
-
2009
+ table.drop(bind=dst_engine)
2010
+ # Delete all data in target database
2011
+ for table in reversed(dst_metadata.sorted_tables):
2012
+ table.delete()
1783
2013
  dst_metadata.clear()
1784
- dst_metadata.reflect()
1785
- src_metadata.reflect()
1786
-
2014
+ dst_metadata.reflect(bind=dst_engine)
2015
+ src_metadata.reflect(bind=src_engine)
1787
2016
  # create all tables in target database
1788
2017
  for table in src_metadata.sorted_tables:
1789
2018
  if table.name not in exclude_tables:
1790
2019
  table.create(bind=dst_engine)
1791
-
1792
2020
  # refresh metadata before you can copy data
1793
2021
  dst_metadata.clear()
1794
- dst_metadata.reflect()
1795
-
2022
+ dst_metadata.reflect(bind=dst_engine)
1796
2023
  # Copy all data from src to target
1797
2024
  print("Copying data...")
1798
2025
  string = "\rCopying table {name}: Inserting row {row}"
2026
+ index = 0
1799
2027
  for table in dst_metadata.sorted_tables:
1800
2028
  src_table = src_metadata.tables[table.name]
1801
2029
  stmt = table.insert()
1802
- index = 0
1803
- for index, row in enumerate(src_table.select().execute()):
2030
+ for index, row in enumerate(src_conn.execute(src_table.select())):
1804
2031
  print(string.format(name=table.name, row=index), end="", flush=True)
1805
- stmt.execute(row._asdict()) # noqa
2032
+ dst_conn.execute(stmt.values(row))
1806
2033
  print(f"\rCopying table {table.name}: Inserted {index} rows", flush=True)
2034
+
2035
+ dst_conn.commit()
@@ -3,6 +3,7 @@
3
3
  # Date: 2023-08-07
4
4
 
5
5
  import logging
6
+ from contextlib import contextmanager
6
7
 
7
8
  logger = logging.getLogger(__name__)
8
9
 
@@ -103,6 +104,27 @@ class RekordboxAgentRegistry:
103
104
  """Disables the tracking of database changes."""
104
105
  cls.__enabled__ = False
105
106
 
107
+ @classmethod
108
+ @contextmanager
109
+ def disabled(cls):
110
+ """Context manager to temporarily disable the tracking of database changes.
111
+
112
+ Examples
113
+ --------
114
+ >>> registry = RekordboxAgentRegistry(db) # noqa
115
+ >>> registry.__enabled__
116
+ True
117
+
118
+ >>> with registry.disabled():
119
+ ... print(registry.__enabled__)
120
+ False
121
+ """
122
+ enabled = cls.__enabled__
123
+ cls.disable_tracking()
124
+ yield cls
125
+ if enabled:
126
+ cls.enable_tracking()
127
+
106
128
  def get_registries(self):
107
129
  """Returns all agent registries.
108
130
 
@@ -285,6 +307,7 @@ class RekordboxAgentRegistry:
285
307
  reg = self.db.get_agent_registry(registry_id="localUpdateCount")
286
308
  usn = reg.int_1
287
309
  self.disable_tracking()
310
+ self.db.flush()
288
311
  with self.db.session.no_autoflush:
289
312
  for instances, op, _, _ in self.__update_sequence__.copy():
290
313
  usn += 1
@@ -298,5 +321,6 @@ class RekordboxAgentRegistry:
298
321
  reg.int_1 = usn
299
322
 
300
323
  self.clear_buffer()
324
+ self.db.flush()
301
325
  self.enable_tracking()
302
326
  return usn