python3-discogs-client 2.7.1__tar.gz → 2.9__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.
Files changed (50) hide show
  1. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/PKG-INFO +11 -2
  2. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/__init__.py +1 -1
  3. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/client.py +11 -0
  4. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/models.py +58 -14
  5. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/test_core.py +40 -0
  6. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/test_models.py +36 -2
  7. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/python3_discogs_client.egg-info/PKG-INFO +11 -2
  8. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/setup.py +1 -1
  9. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/LICENSE +0 -0
  10. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/MANIFEST.in +0 -0
  11. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/exceptions.py +0 -0
  12. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/fetchers.py +0 -0
  13. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/__init__.py +0 -0
  14. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/artists/1/releases_per_page=50&page=1.json +0 -0
  15. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/artists/1/releases_per_page=50&page=2.json +0 -0
  16. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/artists/1.json +0 -0
  17. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/artists/2.json +0 -0
  18. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/artists/3.json +0 -0
  19. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/database/search_artist=trash80&per_page=50&page=1.json +0 -0
  20. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/database/search_artist=trash80&title=icarus&per_page=50&page=1.json +0 -0
  21. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/database/search_q=trash%2080&per_page=50&page=1.json +0 -0
  22. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/database/search_q=trash80&per_page=50&page=1.json +0 -0
  23. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/labels/1.json +0 -0
  24. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/labels/31405.json +0 -0
  25. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/lists/1.json +0 -0
  26. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/marketplace/fee/20.5000/EUR.json +0 -0
  27. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/marketplace/listings/150899904.json +0 -0
  28. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/marketplace/stats/1.json +0 -0
  29. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/masters/4242/versions_per_page=50&page=1.json +0 -0
  30. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/masters/4242.json +0 -0
  31. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/oauth/identity.json +0 -0
  32. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/releases/1.json +0 -0
  33. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/releases/2.json +0 -0
  34. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/releases/3.json +0 -0
  35. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/releases/3329867.json +0 -0
  36. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/releases/79.json +0 -0
  37. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example/collection/folders/0/releases_page=1&per_page=50.json +0 -0
  38. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example/collection/folders.json +0 -0
  39. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example/collection/value.json +0 -0
  40. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example/inventory_per_page=50&page=1.json +0 -0
  41. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example/wants_per_page=50&page=1.json +0 -0
  42. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/res/users/example.json +0 -0
  43. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/test_fetchers.py +0 -0
  44. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/tests/test_utils.py +0 -0
  45. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/discogs_client/utils.py +0 -0
  46. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/python3_discogs_client.egg-info/SOURCES.txt +0 -0
  47. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/python3_discogs_client.egg-info/dependency_links.txt +0 -0
  48. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/python3_discogs_client.egg-info/requires.txt +0 -0
  49. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/python3_discogs_client.egg-info/top_level.txt +0 -0
  50. {python3_discogs_client-2.7.1 → python3_discogs_client-2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python3-discogs-client
3
- Version: 2.7.1
3
+ Version: 2.9
4
4
  Summary: Python API client for Discogs
5
5
  Home-page: https://github.com/joalla/discogs_client
6
6
  Author: joalla
@@ -21,5 +21,14 @@ Provides-Extra: docs
21
21
  Requires-Dist: sphinx; extra == "docs"
22
22
  Requires-Dist: pydata-sphinx-theme; extra == "docs"
23
23
  Requires-Dist: myst-parser; extra == "docs"
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: home-page
29
+ Dynamic: license-file
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-dist
32
+ Dynamic: summary
24
33
 
25
34
  This is an active fork of the official "Discogs API client for Python", which was deprecated by discogs.com as of June 2020. We think it is a very useful Python module and decided to continue maintaining it. Please visit: https://github.com/joalla/discogs_client for more information.
@@ -1,4 +1,4 @@
1
- __version__ = '2.7.1'
1
+ __version__ = '2.9'
2
2
  __version_info__ = tuple(int(i) for i in __version__.split('.') if i.isdigit())
3
3
 
4
4
  from discogs_client.client import Client
@@ -19,6 +19,7 @@ class Client:
19
19
  self.user_agent = user_agent
20
20
  self.verbose = False
21
21
  self._fetcher = RequestsFetcher()
22
+ self._trust_per_page = True # Default: True
22
23
 
23
24
  if consumer_key and consumer_secret:
24
25
  self.set_consumer_key(consumer_key, consumer_secret)
@@ -219,3 +220,13 @@ class Client:
219
220
  """
220
221
  self._fetcher.connect_timeout = connect
221
222
  self._fetcher.read_timeout = read
223
+
224
+ @property
225
+ def trust_per_page(self) -> bool:
226
+ return self._trust_per_page
227
+
228
+ @trust_per_page.setter
229
+ def trust_per_page(self, value: bool) -> None:
230
+ if not isinstance(value, bool):
231
+ raise ValueError("trust_per_page must be a bool")
232
+ self._trust_per_page = value
@@ -354,18 +354,42 @@ class BasePaginatedResponse:
354
354
  return item
355
355
 
356
356
  def __getitem__(self, index):
357
- page_index = index // self.per_page + 1
358
- offset = index % self.per_page
357
+ """Retrieve an item by its index.
359
358
 
360
- try:
361
- page = self.page(page_index)
362
- except HTTPError as e:
363
- if e.status_code == 404:
364
- raise IndexError(e.msg)
365
- else:
359
+ By default, uses the API's ``per_page`` value to calculate the page
360
+ containing the item directly. If the API returns fewer items per page
361
+ than reported, this may yield incorrect results — set
362
+ ``client.trust_per_page = False`` to fall back to a sequential page
363
+ walk at the cost of performance.
364
+ """
365
+ if self.client._trust_per_page:
366
+ page_index = index // self.per_page + 1
367
+ offset = index % self.per_page
368
+
369
+ try:
370
+ page = self.page(page_index)
371
+ except HTTPError as e:
372
+ if e.status_code == 404:
373
+ raise IndexError(e.msg) from e
374
+ raise
375
+
376
+ return page[offset]
377
+
378
+ # Fallback to sequential page loading if we're not trusting the per_page parameter
379
+ current = 0
380
+ page_index = 1
381
+ while True:
382
+ try:
383
+ page = self.page(page_index)
384
+ except HTTPError as e:
385
+ if e.status_code == 404:
386
+ raise IndexError(e.msg) from e
366
387
  raise
388
+ if current + len(page) > index:
389
+ return page[index - current]
390
+ current += len(page)
391
+ page_index += 1
367
392
 
368
- return page[offset]
369
393
 
370
394
  def __len__(self):
371
395
  return self.count
@@ -745,7 +769,7 @@ class CollectionFolder(PrimaryAPIObject):
745
769
  count = SimpleField() #:
746
770
 
747
771
  def __init__(self, client, dict_):
748
- super(CollectionFolder, self).__init__(client, dict_)
772
+ super().__init__(client, dict_)
749
773
 
750
774
  @property
751
775
  def releases(self):
@@ -754,14 +778,34 @@ class CollectionFolder(PrimaryAPIObject):
754
778
 
755
779
  def add_release(self, release):
756
780
  release_id = release.id if isinstance(release, Release) else release
757
- add_release_url = self.fetch('resource_url') + '/releases/{}'.format(release_id)
758
- self.client._post(add_release_url, None)
781
+ resource_url = self.fetch('resource_url')
782
+ self.client._post(f"{resource_url}/releases/{release_id}", None)
759
783
 
760
784
  def remove_release(self, instance):
785
+ """Remove a collection item entirely.
786
+ """
761
787
  if not isinstance(instance, CollectionItemInstance):
762
788
  raise TypeError('instance must be of type CollectionItemInstance')
763
- instance_url = self.fetch('resource_url') + '/releases/{0}/instances/{1}'.format(instance.id, instance.instance_id)
764
- self.client._delete(instance_url)
789
+ resource_url = self.fetch('resource_url')
790
+ self.client._delete(f"{resource_url}/releases/{instance.id}/instances/{instance.instance_id}")
791
+
792
+ def move_release(self, instance, target_folder_id):
793
+ """Move a collection item to another folder.
794
+
795
+ Moving to folder id 1 moves to the "Uncategorized" folder.
796
+ """
797
+ if not isinstance(instance, CollectionItemInstance):
798
+ raise TypeError('instance must be of type CollectionItemInstance')
799
+ resource_url = self.fetch('resource_url')
800
+ self.client._post(
801
+ f"{resource_url}/releases/{instance.id}/instances/{instance.instance_id}",
802
+ {"folder_id": target_folder_id},
803
+ )
804
+
805
+ def uncategorize_release(self, instance):
806
+ """Move a collection item to the "Uncategorized" folder.
807
+ """
808
+ self.move_release(instance, 1)
765
809
 
766
810
  def __repr__(self):
767
811
  return '<CollectionFolder {0!r} {1!r}>'.format(self.id, self.name)
@@ -111,6 +111,46 @@ class CoreTestCase(DiscogsClientTestCase):
111
111
  results.per_page = 10
112
112
  self.assertTrue(results._num_pages is None)
113
113
 
114
+ def test_pagination_index_without_trust_per_page_and_short_pages(self):
115
+ """Without trust_per_page, sequential page walking handles under-filled pages correctly."""
116
+ client = Client('ua')
117
+ client.trust_per_page = False # opt into sequential walk
118
+ client._base_url = ''
119
+ client._trust_per_page = False
120
+ client._fetcher = MemoryFetcher({
121
+ '/artists/1': (
122
+ b'{"id": 1, "name": "Badger", "releases_url": "/artists/1/releases"}',
123
+ 200,
124
+ ),
125
+ '/artists/1/releases?page=1&per_page=50': (
126
+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 5}, '
127
+ b'"releases": ['
128
+ b'{"id": 101, "type": "release", "title": "A"},'
129
+ b'{"id": 102, "type": "release", "title": "B"}'
130
+ b']}',
131
+ 200,
132
+ ),
133
+ '/artists/1/releases?page=2&per_page=50': (
134
+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 5}, '
135
+ b'"releases": [{"id": 103, "type": "release", "title": "C"}]}',
136
+ 200,
137
+ ),
138
+ '/artists/1/releases?page=3&per_page=50': (
139
+ b'{"pagination": {"per_page": 50, "pages": 3, "items": 5}, '
140
+ b'"releases": ['
141
+ b'{"id": 104, "type": "release", "title": "D"},'
142
+ b'{"id": 105, "type": "release", "title": "E"}'
143
+ b']}',
144
+ 200,
145
+ ),
146
+ })
147
+
148
+ results = client.artist(1).releases
149
+ self.assertEqual(results[0].id, 101) # page 1
150
+ self.assertEqual(results[2].id, 103) # page 2, cross-page
151
+ self.assertEqual(results[4].id, 105) # page 3, last item
152
+ self.assertRaises(IndexError, lambda: results[5])
153
+
114
154
  def test_timeout_defaults_to_none(self):
115
155
  # Need to create client without LoggingDelegator here
116
156
  # self.d would throw AttributeError trying to access timeout properties on LoggingDelegator
@@ -1,5 +1,5 @@
1
1
  import unittest
2
- from discogs_client.models import Artist, Release, ListItem, CollectionValue
2
+ from discogs_client.models import Artist, Release, ListItem, CollectionValue, CollectionItemInstance
3
3
  from discogs_client.tests import DiscogsClientTestCase
4
4
  from discogs_client.exceptions import HTTPError
5
5
 
@@ -265,7 +265,6 @@ class ModelsTestCase(DiscogsClientTestCase):
265
265
  self.assertEqual(method, 'GET')
266
266
  self.assertEqual(url, '/marketplace/listings/150899904')
267
267
 
268
-
269
268
  def test_collection(self):
270
269
  """Collection folders can be manipulated"""
271
270
  # Fetch the users collection folders from the filesystem
@@ -314,6 +313,41 @@ class ModelsTestCase(DiscogsClientTestCase):
314
313
  self.assertEqual(method, 'POST')
315
314
  self.assertEqual(url, '/users/example/collection/folders/1/releases/1')
316
315
 
316
+ def test_collection_move_release(self):
317
+ """Collection items can be moved to another folder"""
318
+ # Fetch the users collection folders from the filesystem
319
+ u = self.d.user("example")
320
+ self.assertEqual(u.collection_folders[2].id, 2)
321
+
322
+ # Mock expected responses for move_release test using "MemoryFetcher"
323
+ self.m._fetcher.fetcher.responses = {
324
+ "/users/example/collection/folders": (b'''
325
+ {"folders": [
326
+ {"resource_url": "/users/example/collection/folders/0", "id": 0, "name": "All"},
327
+ {"resource_url": "/users/example/collection/folders/1", "id": 1, "name": "Uncategorized folder"},
328
+ {"resource_url": "/users/example/collection/folders/2", "id": 2, "name": "Collection folder 2"}
329
+ ]}
330
+ ''', 200),
331
+ # Mock the response of the POST request to the instance resource URL
332
+ "/users/example/collection/folders/1/releases/123456/instances/123": (b"", 204),
333
+ }
334
+
335
+ # Bind the user to the MemoryFetcher
336
+ u.client = self.m
337
+
338
+ # Mock a collection item instance
339
+ instance = CollectionItemInstance(
340
+ client=u.client,
341
+ dict_={"id": 123456, "instance_id": 123}
342
+ )
343
+ # Perform the move
344
+ u.collection_folders[1].move_release(instance, 2)
345
+
346
+ # Verify
347
+ method, url, _, _ = self.m._fetcher.last_request
348
+ self.assertEqual(method, "POST")
349
+ self.assertEqual(url, "/users/example/collection/folders/1/releases/123456/instances/123")
350
+
317
351
  def test_delete_object(self):
318
352
  """Can request DELETE on an APIObject"""
319
353
  u = self.d.user('example')
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python3-discogs-client
3
- Version: 2.7.1
3
+ Version: 2.9
4
4
  Summary: Python API client for Discogs
5
5
  Home-page: https://github.com/joalla/discogs_client
6
6
  Author: joalla
@@ -21,5 +21,14 @@ Provides-Extra: docs
21
21
  Requires-Dist: sphinx; extra == "docs"
22
22
  Requires-Dist: pydata-sphinx-theme; extra == "docs"
23
23
  Requires-Dist: myst-parser; extra == "docs"
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: home-page
29
+ Dynamic: license-file
30
+ Dynamic: provides-extra
31
+ Dynamic: requires-dist
32
+ Dynamic: summary
24
33
 
25
34
  This is an active fork of the official "Discogs API client for Python", which was deprecated by discogs.com as of June 2020. We think it is a very useful Python module and decided to continue maintaining it. Please visit: https://github.com/joalla/discogs_client for more information.
@@ -4,7 +4,7 @@ from setuptools import setup
4
4
 
5
5
  setup(
6
6
  name='python3-discogs-client',
7
- version='2.7.1',
7
+ version='2.9',
8
8
  description='Python API client for Discogs',
9
9
  long_description='This is an active fork of the official "Discogs API client for Python", which was deprecated by discogs.com as of June 2020. We think it is a very useful Python module and decided to continue maintaining it. Please visit: https://github.com/joalla/discogs_client for more information.',
10
10
  url='https://github.com/joalla/discogs_client',