elaunira-r2index 0.2.0__tar.gz → 0.3.1__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 (21) hide show
  1. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/PKG-INFO +28 -32
  2. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/README.md +27 -31
  3. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/pyproject.toml +1 -1
  4. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/async_client.py +40 -25
  5. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/async_storage.py +12 -6
  6. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/client.py +40 -25
  7. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/models.py +1 -1
  8. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/storage.py +13 -8
  9. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/test_async_client.py +8 -8
  10. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/test_client.py +16 -16
  11. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/test_download.py +25 -29
  12. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/.gitignore +0 -0
  13. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/scripts/publish.sh +0 -0
  14. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/__init__.py +0 -0
  15. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/__init__.py +0 -0
  16. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/checksums.py +0 -0
  17. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/exceptions.py +0 -0
  18. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/src/elaunira/r2index/py.typed +0 -0
  19. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/__init__.py +0 -0
  20. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/test_checksums.py +0 -0
  21. {elaunira_r2index-0.2.0 → elaunira_r2index-0.3.1}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elaunira-r2index
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Python library for uploading files to R2 and registering them with the r2index API
5
5
  Project-URL: Homepage, https://github.com/elaunira/elaunira-r2-index
6
6
  Project-URL: Repository, https://github.com/elaunira/elaunira-r2-index
@@ -45,23 +45,20 @@ pip install elaunira-r2index
45
45
  ### Sync Client
46
46
 
47
47
  ```python
48
- from elaunira.r2index import R2IndexClient, R2Config
48
+ from elaunira.r2index import R2IndexClient
49
49
 
50
50
  client = R2IndexClient(
51
- api_url="https://r2index.example.com",
52
- api_token="your-bearer-token",
53
- r2_config=R2Config(
54
- access_key_id="your-r2-access-key-id",
55
- secret_access_key="your-r2-secret-access-key",
56
- endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
57
- bucket="your-bucket-name",
58
- ),
51
+ index_api_url="https://r2index.example.com",
52
+ index_api_token="your-bearer-token",
53
+ r2_access_key_id="your-r2-access-key-id",
54
+ r2_secret_access_key="your-r2-secret-access-key",
55
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
59
56
  )
60
57
 
61
58
  # Upload and register a file
62
- record = client.upload_and_register(
63
- local_path="./myfile.zip",
59
+ record = client.upload(
64
60
  bucket="my-bucket",
61
+ local_path="./myfile.zip",
65
62
  category="software",
66
63
  entity="myapp",
67
64
  remote_path="/releases/myapp",
@@ -71,8 +68,8 @@ record = client.upload_and_register(
71
68
  )
72
69
 
73
70
  # Download a file and record the download
74
- # IP address is auto-detected, user agent defaults to "elaunira-r2index/0.1.0"
75
- path, record = client.download_and_record(
71
+ # IP address is auto-detected, user agent defaults to "elaunira-r2index/<version>"
72
+ path, record = client.download(
76
73
  bucket="my-bucket",
77
74
  object_id="/releases/myapp/v1/myapp.zip",
78
75
  destination="./downloads/myfile.zip",
@@ -82,22 +79,19 @@ path, record = client.download_and_record(
82
79
  ### Async Client
83
80
 
84
81
  ```python
85
- from elaunira.r2index import AsyncR2IndexClient, R2Config
82
+ from elaunira.r2index import AsyncR2IndexClient
86
83
 
87
84
  async with AsyncR2IndexClient(
88
- api_url="https://r2index.example.com",
89
- api_token="your-bearer-token",
90
- r2_config=R2Config(
91
- access_key_id="your-r2-access-key-id",
92
- secret_access_key="your-r2-secret-access-key",
93
- endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
94
- bucket="your-bucket-name",
95
- ),
85
+ index_api_url="https://r2index.example.com",
86
+ index_api_token="your-bearer-token",
87
+ r2_access_key_id="your-r2-access-key-id",
88
+ r2_secret_access_key="your-r2-secret-access-key",
89
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
96
90
  ) as client:
97
91
  # Upload
98
- record = await client.upload_and_register(
99
- local_path="./myfile.zip",
92
+ record = await client.upload(
100
93
  bucket="my-bucket",
94
+ local_path="./myfile.zip",
101
95
  category="software",
102
96
  entity="myapp",
103
97
  remote_path="/releases/myapp",
@@ -107,7 +101,7 @@ async with AsyncR2IndexClient(
107
101
  )
108
102
 
109
103
  # Download
110
- path, record = await client.download_and_record(
104
+ path, record = await client.download(
111
105
  bucket="my-bucket",
112
106
  object_id="/releases/myapp/v1/myapp.zip",
113
107
  destination="./downloads/myfile.zip",
@@ -119,12 +113,14 @@ async with AsyncR2IndexClient(
119
113
  Control multipart transfer settings with `R2TransferConfig`:
120
114
 
121
115
  ```python
122
- from elaunira.r2index import R2IndexClient, R2Config, R2TransferConfig
116
+ from elaunira.r2index import R2IndexClient, R2TransferConfig
123
117
 
124
118
  client = R2IndexClient(
125
- api_url="https://r2index.example.com",
126
- api_token="your-bearer-token",
127
- r2_config=R2Config(...),
119
+ index_api_url="https://r2index.example.com",
120
+ index_api_token="your-bearer-token",
121
+ r2_access_key_id="your-r2-access-key-id",
122
+ r2_secret_access_key="your-r2-secret-access-key",
123
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
128
124
  )
129
125
 
130
126
  # Custom transfer settings
@@ -135,7 +131,7 @@ transfer_config = R2TransferConfig(
135
131
  use_threads=True, # Enable threading (default)
136
132
  )
137
133
 
138
- path, record = client.download_and_record(
134
+ path, record = client.download(
139
135
  bucket="my-bucket",
140
136
  object_id="/data/files/v2/largefile.zip",
141
137
  destination="./downloads/largefile.zip",
@@ -151,7 +147,7 @@ Default `max_concurrency` is 2x the number of CPU cores (minimum 4).
151
147
  def on_progress(bytes_transferred: int) -> None:
152
148
  print(f"Downloaded: {bytes_transferred / 1024 / 1024:.1f} MB")
153
149
 
154
- path, record = client.download_and_record(
150
+ path, record = client.download(
155
151
  bucket="my-bucket",
156
152
  object_id="/releases/myapp/v1/myapp.zip",
157
153
  destination="./downloads/myfile.zip",
@@ -13,23 +13,20 @@ pip install elaunira-r2index
13
13
  ### Sync Client
14
14
 
15
15
  ```python
16
- from elaunira.r2index import R2IndexClient, R2Config
16
+ from elaunira.r2index import R2IndexClient
17
17
 
18
18
  client = R2IndexClient(
19
- api_url="https://r2index.example.com",
20
- api_token="your-bearer-token",
21
- r2_config=R2Config(
22
- access_key_id="your-r2-access-key-id",
23
- secret_access_key="your-r2-secret-access-key",
24
- endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
25
- bucket="your-bucket-name",
26
- ),
19
+ index_api_url="https://r2index.example.com",
20
+ index_api_token="your-bearer-token",
21
+ r2_access_key_id="your-r2-access-key-id",
22
+ r2_secret_access_key="your-r2-secret-access-key",
23
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
27
24
  )
28
25
 
29
26
  # Upload and register a file
30
- record = client.upload_and_register(
31
- local_path="./myfile.zip",
27
+ record = client.upload(
32
28
  bucket="my-bucket",
29
+ local_path="./myfile.zip",
33
30
  category="software",
34
31
  entity="myapp",
35
32
  remote_path="/releases/myapp",
@@ -39,8 +36,8 @@ record = client.upload_and_register(
39
36
  )
40
37
 
41
38
  # Download a file and record the download
42
- # IP address is auto-detected, user agent defaults to "elaunira-r2index/0.1.0"
43
- path, record = client.download_and_record(
39
+ # IP address is auto-detected, user agent defaults to "elaunira-r2index/<version>"
40
+ path, record = client.download(
44
41
  bucket="my-bucket",
45
42
  object_id="/releases/myapp/v1/myapp.zip",
46
43
  destination="./downloads/myfile.zip",
@@ -50,22 +47,19 @@ path, record = client.download_and_record(
50
47
  ### Async Client
51
48
 
52
49
  ```python
53
- from elaunira.r2index import AsyncR2IndexClient, R2Config
50
+ from elaunira.r2index import AsyncR2IndexClient
54
51
 
55
52
  async with AsyncR2IndexClient(
56
- api_url="https://r2index.example.com",
57
- api_token="your-bearer-token",
58
- r2_config=R2Config(
59
- access_key_id="your-r2-access-key-id",
60
- secret_access_key="your-r2-secret-access-key",
61
- endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
62
- bucket="your-bucket-name",
63
- ),
53
+ index_api_url="https://r2index.example.com",
54
+ index_api_token="your-bearer-token",
55
+ r2_access_key_id="your-r2-access-key-id",
56
+ r2_secret_access_key="your-r2-secret-access-key",
57
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
64
58
  ) as client:
65
59
  # Upload
66
- record = await client.upload_and_register(
67
- local_path="./myfile.zip",
60
+ record = await client.upload(
68
61
  bucket="my-bucket",
62
+ local_path="./myfile.zip",
69
63
  category="software",
70
64
  entity="myapp",
71
65
  remote_path="/releases/myapp",
@@ -75,7 +69,7 @@ async with AsyncR2IndexClient(
75
69
  )
76
70
 
77
71
  # Download
78
- path, record = await client.download_and_record(
72
+ path, record = await client.download(
79
73
  bucket="my-bucket",
80
74
  object_id="/releases/myapp/v1/myapp.zip",
81
75
  destination="./downloads/myfile.zip",
@@ -87,12 +81,14 @@ async with AsyncR2IndexClient(
87
81
  Control multipart transfer settings with `R2TransferConfig`:
88
82
 
89
83
  ```python
90
- from elaunira.r2index import R2IndexClient, R2Config, R2TransferConfig
84
+ from elaunira.r2index import R2IndexClient, R2TransferConfig
91
85
 
92
86
  client = R2IndexClient(
93
- api_url="https://r2index.example.com",
94
- api_token="your-bearer-token",
95
- r2_config=R2Config(...),
87
+ index_api_url="https://r2index.example.com",
88
+ index_api_token="your-bearer-token",
89
+ r2_access_key_id="your-r2-access-key-id",
90
+ r2_secret_access_key="your-r2-secret-access-key",
91
+ r2_endpoint_url="https://your-account-id.r2.cloudflarestorage.com",
96
92
  )
97
93
 
98
94
  # Custom transfer settings
@@ -103,7 +99,7 @@ transfer_config = R2TransferConfig(
103
99
  use_threads=True, # Enable threading (default)
104
100
  )
105
101
 
106
- path, record = client.download_and_record(
102
+ path, record = client.download(
107
103
  bucket="my-bucket",
108
104
  object_id="/data/files/v2/largefile.zip",
109
105
  destination="./downloads/largefile.zip",
@@ -119,7 +115,7 @@ Default `max_concurrency` is 2x the number of CPU cores (minimum 4).
119
115
  def on_progress(bytes_transferred: int) -> None:
120
116
  print(f"Downloaded: {bytes_transferred / 1024 / 1024:.1f} MB")
121
117
 
122
- path, record = client.download_and_record(
118
+ path, record = client.download(
123
119
  bucket="my-bucket",
124
120
  object_id="/releases/myapp/v1/myapp.zip",
125
121
  destination="./downloads/myfile.zip",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "elaunira-r2index"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "Python library for uploading files to R2 and registering them with the r2index API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -81,29 +81,42 @@ class AsyncR2IndexClient:
81
81
 
82
82
  def __init__(
83
83
  self,
84
- api_url: str,
85
- api_token: str,
86
- r2_config: R2Config | None = None,
84
+ index_api_url: str,
85
+ index_api_token: str,
86
+ r2_access_key_id: str | None = None,
87
+ r2_secret_access_key: str | None = None,
88
+ r2_endpoint_url: str | None = None,
87
89
  timeout: float = 30.0,
88
90
  ) -> None:
89
91
  """
90
92
  Initialize the async R2Index client.
91
93
 
92
94
  Args:
93
- api_url: Base URL of the r2index API.
94
- api_token: Bearer token for authentication.
95
- r2_config: Optional R2 configuration for upload operations.
95
+ index_api_url: Base URL of the r2index API.
96
+ index_api_token: Bearer token for authentication.
97
+ r2_access_key_id: R2 access key ID for storage operations.
98
+ r2_secret_access_key: R2 secret access key for storage operations.
99
+ r2_endpoint_url: R2 endpoint URL for storage operations.
96
100
  timeout: Request timeout in seconds.
97
101
  """
98
- self.api_url = api_url.rstrip("/")
99
- self._token = api_token
102
+ self.api_url = index_api_url.rstrip("/")
103
+ self._token = index_api_token
100
104
  self._timeout = timeout
101
- self._r2_config = r2_config
102
105
  self._storage: AsyncR2Storage | None = None
103
106
 
107
+ # Build R2 config if credentials provided
108
+ if r2_access_key_id and r2_secret_access_key and r2_endpoint_url:
109
+ self._r2_config: R2Config | None = R2Config(
110
+ access_key_id=r2_access_key_id,
111
+ secret_access_key=r2_secret_access_key,
112
+ endpoint_url=r2_endpoint_url,
113
+ )
114
+ else:
115
+ self._r2_config = None
116
+
104
117
  self._client = httpx.AsyncClient(
105
118
  base_url=self.api_url,
106
- headers={"Authorization": f"Bearer {api_token}"},
119
+ headers={"Authorization": f"Bearer {index_api_token}"},
107
120
  timeout=timeout,
108
121
  )
109
122
 
@@ -150,7 +163,7 @@ class AsyncR2IndexClient:
150
163
 
151
164
  # File Operations
152
165
 
153
- async def list_files(
166
+ async def list(
154
167
  self,
155
168
  bucket: str | None = None,
156
169
  category: str | None = None,
@@ -191,7 +204,7 @@ class AsyncR2IndexClient:
191
204
  data = self._handle_response(response)
192
205
  return FileListResponse.model_validate(data)
193
206
 
194
- async def create_file(self, data: FileCreateRequest) -> FileRecord:
207
+ async def create(self, data: FileCreateRequest) -> FileRecord:
195
208
  """
196
209
  Create or upsert a file record.
197
210
 
@@ -205,7 +218,7 @@ class AsyncR2IndexClient:
205
218
  result = self._handle_response(response)
206
219
  return FileRecord.model_validate(result)
207
220
 
208
- async def get_file(self, file_id: str) -> FileRecord:
221
+ async def get(self, file_id: str) -> FileRecord:
209
222
  """
210
223
  Get a file by ID.
211
224
 
@@ -222,7 +235,7 @@ class AsyncR2IndexClient:
222
235
  data = self._handle_response(response)
223
236
  return FileRecord.model_validate(data)
224
237
 
225
- async def update_file(self, file_id: str, data: FileUpdateRequest) -> FileRecord:
238
+ async def update(self, file_id: str, data: FileUpdateRequest) -> FileRecord:
226
239
  """
227
240
  Update a file record.
228
241
 
@@ -240,7 +253,7 @@ class AsyncR2IndexClient:
240
253
  result = self._handle_response(response)
241
254
  return FileRecord.model_validate(result)
242
255
 
243
- async def delete_file(self, file_id: str) -> None:
256
+ async def delete(self, file_id: str) -> None:
244
257
  """
245
258
  Delete a file by ID.
246
259
 
@@ -253,7 +266,7 @@ class AsyncR2IndexClient:
253
266
  response = await self._client.delete(f"/files/{file_id}")
254
267
  self._handle_response(response)
255
268
 
256
- async def delete_file_by_tuple(self, remote_tuple: RemoteTuple) -> None:
269
+ async def delete_by_tuple(self, remote_tuple: RemoteTuple) -> None:
257
270
  """
258
271
  Delete a file by remote tuple.
259
272
 
@@ -272,7 +285,7 @@ class AsyncR2IndexClient:
272
285
  response = await self._client.delete("/files", params=params)
273
286
  self._handle_response(response)
274
287
 
275
- async def get_file_by_tuple(self, remote_tuple: RemoteTuple) -> FileRecord:
288
+ async def get_by_tuple(self, remote_tuple: RemoteTuple) -> FileRecord:
276
289
  """
277
290
  Get a file by remote tuple.
278
291
 
@@ -295,7 +308,7 @@ class AsyncR2IndexClient:
295
308
  data = self._handle_response(response)
296
309
  return FileRecord.model_validate(data)
297
310
 
298
- async def get_index(
311
+ async def index(
299
312
  self,
300
313
  bucket: str | None = None,
301
314
  category: str | None = None,
@@ -497,10 +510,10 @@ class AsyncR2IndexClient:
497
510
 
498
511
  # High-Level Pipeline
499
512
 
500
- async def upload_and_register(
513
+ async def upload(
501
514
  self,
502
- local_path: str | Path,
503
515
  bucket: str,
516
+ local_path: str | Path,
504
517
  category: str,
505
518
  entity: str,
506
519
  remote_path: str,
@@ -521,8 +534,8 @@ class AsyncR2IndexClient:
521
534
  3. Register with r2index API
522
535
 
523
536
  Args:
524
- local_path: Local path to the file to upload.
525
537
  bucket: The S3/R2 bucket name.
538
+ local_path: Local path to the file to upload.
526
539
  category: File category.
527
540
  entity: File entity.
528
541
  remote_path: Remote path in R2 (e.g., "/data/files").
@@ -553,6 +566,7 @@ class AsyncR2IndexClient:
553
566
  # Step 3: Upload to R2
554
567
  await uploader.upload_file(
555
568
  local_path,
569
+ bucket,
556
570
  object_key,
557
571
  content_type=content_type,
558
572
  progress_callback=progress_callback,
@@ -576,7 +590,7 @@ class AsyncR2IndexClient:
576
590
  sha512=checksums.sha512,
577
591
  )
578
592
 
579
- return await self.create_file(create_request)
593
+ return await self.create(create_request)
580
594
 
581
595
  async def _get_public_ip(self) -> str:
582
596
  """Fetch public IP address from checkip.amazonaws.com."""
@@ -584,7 +598,7 @@ class AsyncR2IndexClient:
584
598
  response = await client.get(CHECKIP_URL, timeout=10.0)
585
599
  return response.text.strip()
586
600
 
587
- async def download_and_record(
601
+ async def download(
588
602
  self,
589
603
  bucket: str,
590
604
  object_id: str,
@@ -613,7 +627,7 @@ class AsyncR2IndexClient:
613
627
  destination: Local path where the file will be saved.
614
628
  ip_address: IP address of the downloader. If not provided, fetched
615
629
  from checkip.amazonaws.com.
616
- user_agent: User agent string. Defaults to "elaunira-r2index/0.1.0".
630
+ user_agent: User agent string. Defaults to "elaunira-r2index/<version>".
617
631
  progress_callback: Optional callback for download progress.
618
632
  transfer_config: Optional transfer configuration for multipart/threading.
619
633
 
@@ -638,11 +652,12 @@ class AsyncR2IndexClient:
638
652
  remote_tuple = _parse_object_id(object_id, bucket)
639
653
 
640
654
  # Step 2: Get file record by tuple
641
- file_record = await self.get_file_by_tuple(remote_tuple)
655
+ file_record = await self.get_by_tuple(remote_tuple)
642
656
 
643
657
  # Step 3: Build R2 object key and download
644
658
  object_key = object_id.strip("/")
645
659
  downloaded_path = await storage.download_file(
660
+ bucket,
646
661
  object_key,
647
662
  destination,
648
663
  progress_callback=progress_callback,
@@ -26,6 +26,7 @@ class AsyncR2Storage:
26
26
  async def upload_file(
27
27
  self,
28
28
  file_path: str | Path,
29
+ bucket: str,
29
30
  object_key: str,
30
31
  content_type: str | None = None,
31
32
  progress_callback: Callable[[int], None] | None = None,
@@ -38,6 +39,7 @@ class AsyncR2Storage:
38
39
 
39
40
  Args:
40
41
  file_path: Path to the file to upload.
42
+ bucket: The R2 bucket name.
41
43
  object_key: The key (path) to store the object under in R2.
42
44
  content_type: Optional content type for the object.
43
45
  progress_callback: Optional callback called with bytes uploaded so far.
@@ -78,7 +80,7 @@ class AsyncR2Storage:
78
80
 
79
81
  await client.upload_file(
80
82
  str(file_path),
81
- self.config.bucket,
83
+ bucket,
82
84
  object_key,
83
85
  ExtraArgs=extra_args if extra_args else None,
84
86
  Callback=callback,
@@ -88,11 +90,12 @@ class AsyncR2Storage:
88
90
 
89
91
  return object_key
90
92
 
91
- async def delete_object(self, object_key: str) -> None:
93
+ async def delete_object(self, bucket: str, object_key: str) -> None:
92
94
  """
93
95
  Delete an object from R2 asynchronously.
94
96
 
95
97
  Args:
98
+ bucket: The R2 bucket name.
96
99
  object_key: The key of the object to delete.
97
100
 
98
101
  Raises:
@@ -106,15 +109,16 @@ class AsyncR2Storage:
106
109
  endpoint_url=self.config.endpoint_url,
107
110
  region_name=self.config.region,
108
111
  ) as client:
109
- await client.delete_object(Bucket=self.config.bucket, Key=object_key)
112
+ await client.delete_object(Bucket=bucket, Key=object_key)
110
113
  except Exception as e:
111
114
  raise UploadError(f"Failed to delete object from R2: {e}") from e
112
115
 
113
- async def object_exists(self, object_key: str) -> bool:
116
+ async def object_exists(self, bucket: str, object_key: str) -> bool:
114
117
  """
115
118
  Check if an object exists in R2 asynchronously.
116
119
 
117
120
  Args:
121
+ bucket: The R2 bucket name.
118
122
  object_key: The key of the object to check.
119
123
 
120
124
  Returns:
@@ -128,7 +132,7 @@ class AsyncR2Storage:
128
132
  endpoint_url=self.config.endpoint_url,
129
133
  region_name=self.config.region,
130
134
  ) as client:
131
- await client.head_object(Bucket=self.config.bucket, Key=object_key)
135
+ await client.head_object(Bucket=bucket, Key=object_key)
132
136
  return True
133
137
  except client.exceptions.ClientError as e:
134
138
  if e.response["Error"]["Code"] == "404":
@@ -137,6 +141,7 @@ class AsyncR2Storage:
137
141
 
138
142
  async def download_file(
139
143
  self,
144
+ bucket: str,
140
145
  object_key: str,
141
146
  file_path: str | Path,
142
147
  progress_callback: Callable[[int], None] | None = None,
@@ -146,6 +151,7 @@ class AsyncR2Storage:
146
151
  Download a file from R2 asynchronously.
147
152
 
148
153
  Args:
154
+ bucket: The R2 bucket name.
149
155
  object_key: The key (path) of the object in R2.
150
156
  file_path: Local path where the file will be saved.
151
157
  progress_callback: Optional callback called with bytes downloaded so far.
@@ -181,7 +187,7 @@ class AsyncR2Storage:
181
187
  callback = _AsyncProgressCallback(progress_callback)
182
188
 
183
189
  await client.download_file(
184
- self.config.bucket,
190
+ bucket,
185
191
  object_key,
186
192
  str(file_path),
187
193
  Callback=callback,
@@ -80,29 +80,42 @@ class R2IndexClient:
80
80
 
81
81
  def __init__(
82
82
  self,
83
- api_url: str,
84
- api_token: str,
85
- r2_config: R2Config | None = None,
83
+ index_api_url: str,
84
+ index_api_token: str,
85
+ r2_access_key_id: str | None = None,
86
+ r2_secret_access_key: str | None = None,
87
+ r2_endpoint_url: str | None = None,
86
88
  timeout: float = 30.0,
87
89
  ) -> None:
88
90
  """
89
91
  Initialize the R2Index client.
90
92
 
91
93
  Args:
92
- api_url: Base URL of the r2index API.
93
- api_token: Bearer token for authentication.
94
- r2_config: Optional R2 configuration for upload operations.
94
+ index_api_url: Base URL of the r2index API.
95
+ index_api_token: Bearer token for authentication.
96
+ r2_access_key_id: R2 access key ID for storage operations.
97
+ r2_secret_access_key: R2 secret access key for storage operations.
98
+ r2_endpoint_url: R2 endpoint URL for storage operations.
95
99
  timeout: Request timeout in seconds.
96
100
  """
97
- self.api_url = api_url.rstrip("/")
98
- self._token = api_token
101
+ self.api_url = index_api_url.rstrip("/")
102
+ self._token = index_api_token
99
103
  self._timeout = timeout
100
- self._r2_config = r2_config
101
104
  self._storage: R2Storage | None = None
102
105
 
106
+ # Build R2 config if credentials provided
107
+ if r2_access_key_id and r2_secret_access_key and r2_endpoint_url:
108
+ self._r2_config: R2Config | None = R2Config(
109
+ access_key_id=r2_access_key_id,
110
+ secret_access_key=r2_secret_access_key,
111
+ endpoint_url=r2_endpoint_url,
112
+ )
113
+ else:
114
+ self._r2_config = None
115
+
103
116
  self._client = httpx.Client(
104
117
  base_url=self.api_url,
105
- headers={"Authorization": f"Bearer {api_token}"},
118
+ headers={"Authorization": f"Bearer {index_api_token}"},
106
119
  timeout=timeout,
107
120
  )
108
121
 
@@ -149,7 +162,7 @@ class R2IndexClient:
149
162
 
150
163
  # File Operations
151
164
 
152
- def list_files(
165
+ def list(
153
166
  self,
154
167
  bucket: str | None = None,
155
168
  category: str | None = None,
@@ -190,7 +203,7 @@ class R2IndexClient:
190
203
  data = self._handle_response(response)
191
204
  return FileListResponse.model_validate(data)
192
205
 
193
- def create_file(self, data: FileCreateRequest) -> FileRecord:
206
+ def create(self, data: FileCreateRequest) -> FileRecord:
194
207
  """
195
208
  Create or upsert a file record.
196
209
 
@@ -204,7 +217,7 @@ class R2IndexClient:
204
217
  result = self._handle_response(response)
205
218
  return FileRecord.model_validate(result)
206
219
 
207
- def get_file(self, file_id: str) -> FileRecord:
220
+ def get(self, file_id: str) -> FileRecord:
208
221
  """
209
222
  Get a file by ID.
210
223
 
@@ -221,7 +234,7 @@ class R2IndexClient:
221
234
  data = self._handle_response(response)
222
235
  return FileRecord.model_validate(data)
223
236
 
224
- def update_file(self, file_id: str, data: FileUpdateRequest) -> FileRecord:
237
+ def update(self, file_id: str, data: FileUpdateRequest) -> FileRecord:
225
238
  """
226
239
  Update a file record.
227
240
 
@@ -239,7 +252,7 @@ class R2IndexClient:
239
252
  result = self._handle_response(response)
240
253
  return FileRecord.model_validate(result)
241
254
 
242
- def delete_file(self, file_id: str) -> None:
255
+ def delete(self, file_id: str) -> None:
243
256
  """
244
257
  Delete a file by ID.
245
258
 
@@ -252,7 +265,7 @@ class R2IndexClient:
252
265
  response = self._client.delete(f"/files/{file_id}")
253
266
  self._handle_response(response)
254
267
 
255
- def delete_file_by_tuple(self, remote_tuple: RemoteTuple) -> None:
268
+ def delete_by_tuple(self, remote_tuple: RemoteTuple) -> None:
256
269
  """
257
270
  Delete a file by remote tuple.
258
271
 
@@ -271,7 +284,7 @@ class R2IndexClient:
271
284
  response = self._client.delete("/files", params=params)
272
285
  self._handle_response(response)
273
286
 
274
- def get_file_by_tuple(self, remote_tuple: RemoteTuple) -> FileRecord:
287
+ def get_by_tuple(self, remote_tuple: RemoteTuple) -> FileRecord:
275
288
  """
276
289
  Get a file by remote tuple.
277
290
 
@@ -294,7 +307,7 @@ class R2IndexClient:
294
307
  data = self._handle_response(response)
295
308
  return FileRecord.model_validate(data)
296
309
 
297
- def get_index(
310
+ def index(
298
311
  self,
299
312
  bucket: str | None = None,
300
313
  category: str | None = None,
@@ -496,10 +509,10 @@ class R2IndexClient:
496
509
 
497
510
  # High-Level Pipeline
498
511
 
499
- def upload_and_register(
512
+ def upload(
500
513
  self,
501
- local_path: str | Path,
502
514
  bucket: str,
515
+ local_path: str | Path,
503
516
  category: str,
504
517
  entity: str,
505
518
  remote_path: str,
@@ -520,8 +533,8 @@ class R2IndexClient:
520
533
  3. Register with r2index API
521
534
 
522
535
  Args:
523
- local_path: Local path to the file to upload.
524
536
  bucket: The S3/R2 bucket name.
537
+ local_path: Local path to the file to upload.
525
538
  category: File category.
526
539
  entity: File entity.
527
540
  remote_path: Remote path in R2 (e.g., "/data/files").
@@ -552,6 +565,7 @@ class R2IndexClient:
552
565
  # Step 3: Upload to R2
553
566
  uploader.upload_file(
554
567
  local_path,
568
+ bucket,
555
569
  object_key,
556
570
  content_type=content_type,
557
571
  progress_callback=progress_callback,
@@ -575,14 +589,14 @@ class R2IndexClient:
575
589
  sha512=checksums.sha512,
576
590
  )
577
591
 
578
- return self.create_file(create_request)
592
+ return self.create(create_request)
579
593
 
580
594
  def _get_public_ip(self) -> str:
581
595
  """Fetch public IP address from checkip.amazonaws.com."""
582
596
  response = httpx.get(CHECKIP_URL, timeout=10.0)
583
597
  return response.text.strip()
584
598
 
585
- def download_and_record(
599
+ def download(
586
600
  self,
587
601
  bucket: str,
588
602
  object_id: str,
@@ -611,7 +625,7 @@ class R2IndexClient:
611
625
  destination: Local path where the file will be saved.
612
626
  ip_address: IP address of the downloader. If not provided, fetched
613
627
  from checkip.amazonaws.com.
614
- user_agent: User agent string. Defaults to "elaunira-r2index/0.1.0".
628
+ user_agent: User agent string. Defaults to "elaunira-r2index/<version>".
615
629
  progress_callback: Optional callback for download progress.
616
630
  transfer_config: Optional transfer configuration for multipart/threading.
617
631
 
@@ -636,11 +650,12 @@ class R2IndexClient:
636
650
  remote_tuple = _parse_object_id(object_id, bucket)
637
651
 
638
652
  # Step 2: Get file record by tuple
639
- file_record = self.get_file_by_tuple(remote_tuple)
653
+ file_record = self.get_by_tuple(remote_tuple)
640
654
 
641
655
  # Step 3: Build R2 object key and download
642
656
  object_key = object_id.strip("/")
643
657
  downloaded_path = storage.download_file(
658
+ bucket,
644
659
  object_key,
645
660
  destination,
646
661
  progress_callback=progress_callback,
@@ -206,4 +206,4 @@ class HealthResponse(BaseModel):
206
206
  """Response for health check."""
207
207
 
208
208
  status: str
209
- timestamp: datetime
209
+ timestamp: datetime | None = None
@@ -43,9 +43,8 @@ class R2Config:
43
43
  """Configuration for R2 storage."""
44
44
 
45
45
  access_key_id: str
46
- secret_access_key: str
47
46
  endpoint_url: str
48
- bucket: str
47
+ secret_access_key: str
49
48
  region: str = "auto"
50
49
 
51
50
 
@@ -71,6 +70,7 @@ class R2Storage:
71
70
  def upload_file(
72
71
  self,
73
72
  file_path: str | Path,
73
+ bucket: str,
74
74
  object_key: str,
75
75
  content_type: str | None = None,
76
76
  progress_callback: Callable[[int], None] | None = None,
@@ -83,6 +83,7 @@ class R2Storage:
83
83
 
84
84
  Args:
85
85
  file_path: Path to the file to upload.
86
+ bucket: The R2 bucket name.
86
87
  object_key: The key (path) to store the object under in R2.
87
88
  content_type: Optional content type for the object.
88
89
  progress_callback: Optional callback called with bytes uploaded so far.
@@ -118,7 +119,7 @@ class R2Storage:
118
119
  try:
119
120
  self._client.upload_file(
120
121
  str(file_path),
121
- self.config.bucket,
122
+ bucket,
122
123
  object_key,
123
124
  Config=boto_transfer_config,
124
125
  ExtraArgs=extra_args if extra_args else None,
@@ -129,33 +130,35 @@ class R2Storage:
129
130
 
130
131
  return object_key
131
132
 
132
- def delete_object(self, object_key: str) -> None:
133
+ def delete_object(self, bucket: str, object_key: str) -> None:
133
134
  """
134
135
  Delete an object from R2.
135
136
 
136
137
  Args:
138
+ bucket: The R2 bucket name.
137
139
  object_key: The key of the object to delete.
138
140
 
139
141
  Raises:
140
142
  UploadError: If the deletion fails.
141
143
  """
142
144
  try:
143
- self._client.delete_object(Bucket=self.config.bucket, Key=object_key)
145
+ self._client.delete_object(Bucket=bucket, Key=object_key)
144
146
  except Exception as e:
145
147
  raise UploadError(f"Failed to delete object from R2: {e}") from e
146
148
 
147
- def object_exists(self, object_key: str) -> bool:
149
+ def object_exists(self, bucket: str, object_key: str) -> bool:
148
150
  """
149
151
  Check if an object exists in R2.
150
152
 
151
153
  Args:
154
+ bucket: The R2 bucket name.
152
155
  object_key: The key of the object to check.
153
156
 
154
157
  Returns:
155
158
  True if the object exists, False otherwise.
156
159
  """
157
160
  try:
158
- self._client.head_object(Bucket=self.config.bucket, Key=object_key)
161
+ self._client.head_object(Bucket=bucket, Key=object_key)
159
162
  return True
160
163
  except self._client.exceptions.ClientError as e:
161
164
  if e.response["Error"]["Code"] == "404":
@@ -164,6 +167,7 @@ class R2Storage:
164
167
 
165
168
  def download_file(
166
169
  self,
170
+ bucket: str,
167
171
  object_key: str,
168
172
  file_path: str | Path,
169
173
  progress_callback: Callable[[int], None] | None = None,
@@ -173,6 +177,7 @@ class R2Storage:
173
177
  Download a file from R2.
174
178
 
175
179
  Args:
180
+ bucket: The R2 bucket name.
176
181
  object_key: The key (path) of the object in R2.
177
182
  file_path: Local path where the file will be saved.
178
183
  progress_callback: Optional callback called with bytes downloaded so far.
@@ -203,7 +208,7 @@ class R2Storage:
203
208
 
204
209
  try:
205
210
  self._client.download_file(
206
- self.config.bucket,
211
+ bucket,
207
212
  object_key,
208
213
  str(file_path),
209
214
  Config=boto_transfer_config,
@@ -10,8 +10,8 @@ from elaunira.r2index import AsyncR2IndexClient, FileCreateRequest
10
10
  def async_client():
11
11
  """Create a test async client."""
12
12
  return AsyncR2IndexClient(
13
- api_url="https://api.example.com",
14
- api_token="test-token",
13
+ index_api_url="https://api.example.com",
14
+ index_api_token="test-token",
15
15
  )
16
16
 
17
17
 
@@ -19,14 +19,14 @@ def async_client():
19
19
  async def test_async_client_context_manager():
20
20
  """Test async client as context manager."""
21
21
  async with AsyncR2IndexClient(
22
- api_url="https://api.example.com",
23
- api_token="test-token",
22
+ index_api_url="https://api.example.com",
23
+ index_api_token="test-token",
24
24
  ) as client:
25
25
  assert client is not None
26
26
 
27
27
 
28
28
  @pytest.mark.asyncio
29
- async def test_async_list_files(async_client: AsyncR2IndexClient, httpx_mock: HTTPXMock):
29
+ async def test_async_list(async_client: AsyncR2IndexClient, httpx_mock: HTTPXMock):
30
30
  """Test async listing files."""
31
31
  httpx_mock.add_response(
32
32
  url="https://api.example.com/files",
@@ -56,13 +56,13 @@ async def test_async_list_files(async_client: AsyncR2IndexClient, httpx_mock: HT
56
56
  },
57
57
  )
58
58
 
59
- response = await async_client.list_files()
59
+ response = await async_client.list()
60
60
  assert len(response.files) == 1
61
61
  await async_client.close()
62
62
 
63
63
 
64
64
  @pytest.mark.asyncio
65
- async def test_async_create_file(async_client: AsyncR2IndexClient, httpx_mock: HTTPXMock):
65
+ async def test_async_create(async_client: AsyncR2IndexClient, httpx_mock: HTTPXMock):
66
66
  """Test async creating a file record."""
67
67
  httpx_mock.add_response(
68
68
  url="https://api.example.com/files",
@@ -100,7 +100,7 @@ async def test_async_create_file(async_client: AsyncR2IndexClient, httpx_mock: H
100
100
  sha256="ghi",
101
101
  sha512="jkl",
102
102
  )
103
- record = await async_client.create_file(request)
103
+ record = await async_client.create(request)
104
104
  assert record.id == "new-file"
105
105
  await async_client.close()
106
106
 
@@ -16,8 +16,8 @@ from elaunira.r2index import (
16
16
  def client():
17
17
  """Create a test client."""
18
18
  return R2IndexClient(
19
- api_url="https://api.example.com",
20
- api_token="test-token",
19
+ index_api_url="https://api.example.com",
20
+ index_api_token="test-token",
21
21
  )
22
22
 
23
23
 
@@ -29,13 +29,13 @@ def test_client_initialization(client: R2IndexClient):
29
29
  def test_client_context_manager():
30
30
  """Test client as context manager."""
31
31
  with R2IndexClient(
32
- api_url="https://api.example.com",
33
- api_token="test-token",
32
+ index_api_url="https://api.example.com",
33
+ index_api_token="test-token",
34
34
  ) as client:
35
35
  assert client is not None
36
36
 
37
37
 
38
- def test_list_files(client: R2IndexClient, httpx_mock: HTTPXMock):
38
+ def test_list(client: R2IndexClient, httpx_mock: HTTPXMock):
39
39
  """Test listing files."""
40
40
  httpx_mock.add_response(
41
41
  url="https://api.example.com/files",
@@ -65,19 +65,19 @@ def test_list_files(client: R2IndexClient, httpx_mock: HTTPXMock):
65
65
  },
66
66
  )
67
67
 
68
- response = client.list_files()
68
+ response = client.list()
69
69
  assert len(response.files) == 1
70
70
  assert response.files[0].id == "file1"
71
71
 
72
72
 
73
- def test_list_files_with_filters(client: R2IndexClient, httpx_mock: HTTPXMock):
73
+ def test_list_with_filters(client: R2IndexClient, httpx_mock: HTTPXMock):
74
74
  """Test listing files with filters."""
75
75
  httpx_mock.add_response(
76
76
  url="https://api.example.com/files?category=software&entity=myapp&tags=release%2Cstable",
77
77
  json={"files": [], "total": 0, "page": 1, "pageSize": 20},
78
78
  )
79
79
 
80
- response = client.list_files(
80
+ response = client.list(
81
81
  category="software",
82
82
  entity="myapp",
83
83
  tags=["release", "stable"],
@@ -85,7 +85,7 @@ def test_list_files_with_filters(client: R2IndexClient, httpx_mock: HTTPXMock):
85
85
  assert response.total == 0
86
86
 
87
87
 
88
- def test_create_file(client: R2IndexClient, httpx_mock: HTTPXMock):
88
+ def test_create(client: R2IndexClient, httpx_mock: HTTPXMock):
89
89
  """Test creating a file record."""
90
90
  httpx_mock.add_response(
91
91
  url="https://api.example.com/files",
@@ -123,11 +123,11 @@ def test_create_file(client: R2IndexClient, httpx_mock: HTTPXMock):
123
123
  sha256="ghi",
124
124
  sha512="jkl",
125
125
  )
126
- record = client.create_file(request)
126
+ record = client.create(request)
127
127
  assert record.id == "new-file"
128
128
 
129
129
 
130
- def test_get_file(client: R2IndexClient, httpx_mock: HTTPXMock):
130
+ def test_get(client: R2IndexClient, httpx_mock: HTTPXMock):
131
131
  """Test getting a file by ID."""
132
132
  httpx_mock.add_response(
133
133
  url="https://api.example.com/files/file123",
@@ -150,11 +150,11 @@ def test_get_file(client: R2IndexClient, httpx_mock: HTTPXMock):
150
150
  },
151
151
  )
152
152
 
153
- record = client.get_file("file123")
153
+ record = client.get("file123")
154
154
  assert record.id == "file123"
155
155
 
156
156
 
157
- def test_get_file_not_found(client: R2IndexClient, httpx_mock: HTTPXMock):
157
+ def test_get_not_found(client: R2IndexClient, httpx_mock: HTTPXMock):
158
158
  """Test 404 error handling."""
159
159
  httpx_mock.add_response(
160
160
  url="https://api.example.com/files/notfound",
@@ -163,7 +163,7 @@ def test_get_file_not_found(client: R2IndexClient, httpx_mock: HTTPXMock):
163
163
  )
164
164
 
165
165
  with pytest.raises(NotFoundError) as exc_info:
166
- client.get_file("notfound")
166
+ client.get("notfound")
167
167
 
168
168
  assert exc_info.value.status_code == 404
169
169
 
@@ -177,7 +177,7 @@ def test_authentication_error(client: R2IndexClient, httpx_mock: HTTPXMock):
177
177
  )
178
178
 
179
179
  with pytest.raises(AuthenticationError) as exc_info:
180
- client.list_files()
180
+ client.list()
181
181
 
182
182
  assert exc_info.value.status_code == 401
183
183
 
@@ -206,7 +206,7 @@ def test_validation_error(client: R2IndexClient, httpx_mock: HTTPXMock):
206
206
  )
207
207
 
208
208
  with pytest.raises(ValidationError) as exc_info:
209
- client.create_file(request)
209
+ client.create(request)
210
210
 
211
211
  assert exc_info.value.status_code == 400
212
212
 
@@ -11,7 +11,7 @@ from elaunira.r2index import (
11
11
  RemoteTuple,
12
12
  )
13
13
  from elaunira.r2index.client import _parse_object_id
14
- from elaunira.r2index.storage import R2Config, R2TransferConfig
14
+ from elaunira.r2index.storage import R2TransferConfig
15
15
 
16
16
 
17
17
  class TestParseObjectId:
@@ -74,18 +74,18 @@ class TestParseObjectId:
74
74
  _parse_object_id("", "test-bucket")
75
75
 
76
76
 
77
- class TestGetFileByTuple:
78
- """Tests for get_file_by_tuple method."""
77
+ class TestGetByTuple:
78
+ """Tests for get_by_tuple method."""
79
79
 
80
80
  @pytest.fixture
81
81
  def client(self):
82
82
  """Create a test client."""
83
83
  return R2IndexClient(
84
- api_url="https://api.example.com",
85
- api_token="test-token",
84
+ index_api_url="https://api.example.com",
85
+ index_api_token="test-token",
86
86
  )
87
87
 
88
- def test_get_file_by_tuple(self, client: R2IndexClient, httpx_mock: HTTPXMock):
88
+ def test_get_by_tuple(self, client: R2IndexClient, httpx_mock: HTTPXMock):
89
89
  """Test getting a file by remote tuple."""
90
90
  httpx_mock.add_response(
91
91
  url="https://api.example.com/files/by-tuple?bucket=test-bucket&remotePath=%2Freleases%2Fmyapp&remoteFilename=myapp.zip&remoteVersion=v1",
@@ -114,7 +114,7 @@ class TestGetFileByTuple:
114
114
  remote_filename="myapp.zip",
115
115
  remote_version="v1",
116
116
  )
117
- record = client.get_file_by_tuple(remote_tuple)
117
+ record = client.get_by_tuple(remote_tuple)
118
118
 
119
119
  assert record.id == "file123"
120
120
  assert record.bucket == "test-bucket"
@@ -123,35 +123,31 @@ class TestGetFileByTuple:
123
123
  assert record.remote_version == "v1"
124
124
 
125
125
 
126
- class TestDownloadAndRecord:
127
- """Tests for download_and_record method."""
126
+ class TestDownload:
127
+ """Tests for download method."""
128
128
 
129
129
  @pytest.fixture
130
130
  def client_with_r2(self):
131
131
  """Create a test client with R2 config."""
132
- r2_config = R2Config(
133
- access_key_id="test-key",
134
- secret_access_key="test-secret",
135
- endpoint_url="https://r2.example.com",
136
- bucket="test-bucket",
137
- )
138
132
  return R2IndexClient(
139
- api_url="https://api.example.com",
140
- api_token="test-token",
141
- r2_config=r2_config,
133
+ index_api_url="https://api.example.com",
134
+ index_api_token="test-token",
135
+ r2_access_key_id="test-key",
136
+ r2_secret_access_key="test-secret",
137
+ r2_endpoint_url="https://r2.example.com",
142
138
  )
143
139
 
144
- def test_download_and_record_with_defaults(
140
+ def test_download_with_defaults(
145
141
  self, client_with_r2: R2IndexClient, httpx_mock: HTTPXMock, tmp_path: Path
146
142
  ):
147
- """Test download_and_record with default IP and user agent."""
143
+ """Test download with default IP and user agent."""
148
144
  # Mock checkip.amazonaws.com
149
145
  httpx_mock.add_response(
150
146
  url="https://checkip.amazonaws.com",
151
147
  text="203.0.113.1\n",
152
148
  )
153
149
 
154
- # Mock get_file_by_tuple
150
+ # Mock get_by_tuple
155
151
  httpx_mock.add_response(
156
152
  url="https://api.example.com/files/by-tuple?bucket=test-bucket&remotePath=%2Freleases%2Fmyapp&remoteFilename=myapp.zip&remoteVersion=v1",
157
153
  json={
@@ -195,7 +191,7 @@ class TestDownloadAndRecord:
195
191
  "download_file",
196
192
  return_value=destination,
197
193
  ) as mock_download:
198
- downloaded_path, file_record = client_with_r2.download_and_record(
194
+ downloaded_path, file_record = client_with_r2.download(
199
195
  bucket="test-bucket",
200
196
  object_id="/releases/myapp/v1/myapp.zip",
201
197
  destination=str(destination),
@@ -205,11 +201,11 @@ class TestDownloadAndRecord:
205
201
  assert downloaded_path == destination
206
202
  assert file_record.id == "file123"
207
203
 
208
- def test_download_and_record_with_explicit_ip_and_user_agent(
204
+ def test_download_with_explicit_ip_and_user_agent(
209
205
  self, client_with_r2: R2IndexClient, httpx_mock: HTTPXMock, tmp_path: Path
210
206
  ):
211
- """Test download_and_record with explicit IP and user agent."""
212
- # Mock get_file_by_tuple
207
+ """Test download with explicit IP and user agent."""
208
+ # Mock get_by_tuple
213
209
  httpx_mock.add_response(
214
210
  url="https://api.example.com/files/by-tuple?bucket=test-bucket&remotePath=%2Freleases%2Fmyapp&remoteFilename=myapp.zip&remoteVersion=v1",
215
211
  json={
@@ -253,7 +249,7 @@ class TestDownloadAndRecord:
253
249
  "download_file",
254
250
  return_value=destination,
255
251
  ):
256
- downloaded_path, file_record = client_with_r2.download_and_record(
252
+ downloaded_path, file_record = client_with_r2.download(
257
253
  bucket="test-bucket",
258
254
  object_id="/releases/myapp/v1/myapp.zip",
259
255
  destination=str(destination),
@@ -264,14 +260,14 @@ class TestDownloadAndRecord:
264
260
  assert downloaded_path == destination
265
261
  assert file_record.id == "file123"
266
262
 
267
- def test_download_and_record_invalid_object_id(
263
+ def test_download_invalid_object_id(
268
264
  self, client_with_r2: R2IndexClient, tmp_path: Path
269
265
  ):
270
- """Test download_and_record with invalid object ID."""
266
+ """Test download with invalid object ID."""
271
267
  destination = tmp_path / "file.zip"
272
268
 
273
269
  with pytest.raises(ValueError) as exc_info:
274
- client_with_r2.download_and_record(
270
+ client_with_r2.download(
275
271
  bucket="test-bucket",
276
272
  object_id="/invalid/path",
277
273
  destination=str(destination),