tracktolib 0.68.0__py3-none-any.whl → 0.69.0__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.
tracktolib/http_utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import typing
2
2
  import warnings
3
- from io import TextIOWrapper, BufferedWriter
3
+ from io import BufferedWriter, TextIOWrapper
4
4
  from typing import BinaryIO, Callable, TextIO
5
5
 
6
6
  warnings.warn(
tracktolib/logs.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from dataclasses import dataclass
3
- from typing import Literal, overload, Any, TypeGuard
3
+ from typing import Any, Literal, TypeGuard, overload
4
4
 
5
5
  try:
6
6
  from pythonjsonlogger.json import JsonFormatter
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import re
6
6
  from typing import TYPE_CHECKING, Any, Sequence
7
7
 
8
+ from ..utils import get_chunks
8
9
  from .blocks import (
9
10
  BulletedListBlock,
10
11
  DividerBlock,
@@ -21,7 +22,6 @@ from .blocks import (
21
22
  make_quote_block,
22
23
  make_todo_block,
23
24
  )
24
- from ..utils import get_chunks
25
25
 
26
26
  # Union type for all Notion blocks used in markdown conversion
27
27
  NotionBlock = ParagraphBlock | DividerBlock | BulletedListBlock | NumberedListBlock | TodoBlock | QuoteBlock
@@ -32,11 +32,11 @@ if TYPE_CHECKING:
32
32
  __all__ = [
33
33
  "NOTION_CHAR_LIMIT",
34
34
  "NotionBlock",
35
- "rich_text_to_markdown",
36
- "markdown_to_blocks",
37
35
  "blocks_to_markdown",
38
36
  "blocks_to_markdown_with_comments",
39
37
  "comments_to_markdown",
38
+ "markdown_to_blocks",
39
+ "rich_text_to_markdown",
40
40
  "strip_comments_from_markdown",
41
41
  ]
42
42
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from typing import Any, Literal, NotRequired, TypedDict
4
4
 
5
-
6
5
  # Base types
7
6
 
8
7
 
@@ -2,16 +2,16 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast
6
8
 
7
- import asyncio
8
9
  import niquests
9
- from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast
10
10
 
11
11
  from .markdown import (
12
- markdown_to_blocks,
13
12
  blocks_to_markdown_with_comments,
14
13
  comments_to_markdown,
14
+ markdown_to_blocks,
15
15
  strip_comments_from_markdown,
16
16
  )
17
17
 
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
  from .cache import NotionCache
20
20
  from .models import Block, Comment, PartialBlock
21
21
 
22
+ from ..utils import get_chunks, run_coros
22
23
  from .blocks import (
23
24
  ExportResult,
24
25
  find_divergence_index,
@@ -32,11 +33,10 @@ from .fetch import (
32
33
  fetch_comments,
33
34
  fetch_user,
34
35
  )
35
- from ..utils import get_chunks, run_coros
36
36
 
37
37
  __all__ = [
38
- "ClearResult",
39
38
  "DEFAULT_CONCURRENCY",
39
+ "ClearResult",
40
40
  "PageComment",
41
41
  "ProgressCallback",
42
42
  "UpdateResult",
tracktolib/pg/__init__.py CHANGED
@@ -1,17 +1,17 @@
1
1
  from .query import (
2
- insert_many,
3
- insert_one,
2
+ Conflict,
3
+ OnConflict,
4
+ PGConflictQuery,
4
5
  PGInsertQuery,
5
6
  PGReturningQuery,
6
- PGConflictQuery,
7
- insert_returning,
8
- Conflict,
9
- fetch_count,
10
7
  PGUpdateQuery,
11
- update_returning,
12
- update_one,
8
+ fetch_count,
9
+ insert_many,
10
+ insert_one,
13
11
  insert_pg,
14
- OnConflict,
12
+ insert_returning,
15
13
  update_many,
14
+ update_one,
15
+ update_returning,
16
16
  )
17
- from .utils import iterate_pg, upsert_csv, safe_pg, safe_pg_context, PGError, PGException
17
+ from .utils import PGError, PGException, iterate_pg, safe_pg, safe_pg_context, upsert_csv
tracktolib/pg/query.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import typing
2
2
  from dataclasses import dataclass, field
3
- from typing import Iterable, Callable, Iterator, TypeAlias, overload, Any, Literal
3
+ from typing import Any, Callable, Iterable, Iterator, Literal, TypeAlias, overload
4
4
 
5
5
  from ..pg_utils import get_conflict_query
6
6
 
tracktolib/pg/utils.py CHANGED
@@ -2,11 +2,11 @@ import csv
2
2
  import datetime as dt
3
3
  import functools
4
4
  import logging
5
- from pathlib import Path
6
- from typing import AsyncIterator, Iterable, cast, NamedTuple, Sequence
7
- from typing_extensions import LiteralString
8
- from dataclasses import dataclass
9
5
  from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import AsyncIterator, Iterable, LiteralString, NamedTuple, Sequence, cast
9
+
10
10
  from ..pg_utils import get_conflict_query
11
11
 
12
12
  try:
@@ -21,8 +21,8 @@ from asyncpg.exceptions import (
21
21
  UniqueViolationError,
22
22
  )
23
23
 
24
- from tracktolib.utils import get_chunks
25
24
  from tracktolib.pg_utils import get_tmp_table_query
25
+ from tracktolib.utils import get_chunks
26
26
 
27
27
  logger = logging.Logger("tracktolib-pg")
28
28
 
tracktolib/pg_sync.py CHANGED
@@ -1,13 +1,11 @@
1
1
  from pathlib import Path
2
- from typing import Iterable, Any, overload, Literal, cast, Optional, Mapping, Sequence
3
-
4
- from typing_extensions import LiteralString
2
+ from typing import Any, Iterable, Literal, LiteralString, Mapping, Optional, Sequence, cast, overload
5
3
 
6
4
  try:
7
5
  from psycopg import Connection, Cursor
8
6
  from psycopg.abc import Query, QueryNoTemplate
9
7
  from psycopg.errors import InvalidCatalogName
10
- from psycopg.rows import dict_row, DictRow, TupleRow
8
+ from psycopg.rows import DictRow, TupleRow, dict_row
11
9
  from psycopg.types.json import Json
12
10
  except ImportError:
13
11
  raise ImportError('Please install psycopg or tracktolib with "pg-sync" to use this module')
@@ -22,9 +20,9 @@ __all__ = (
22
20
  "fetch_one",
23
21
  "get_insert_data",
24
22
  "get_tables",
23
+ "insert_csv",
25
24
  "insert_many",
26
25
  "insert_one",
27
- "insert_csv",
28
26
  "set_seq_max",
29
27
  )
30
28
 
tracktolib/pg_utils.py CHANGED
@@ -1,7 +1,4 @@
1
- from typing import Iterable
2
- from typing import cast
3
-
4
- from typing_extensions import LiteralString
1
+ from typing import Iterable, LiteralString, cast
5
2
 
6
3
 
7
4
  def get_tmp_table_query(
tracktolib/s3/minio.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from pathlib import Path
2
2
 
3
3
  try:
4
- from minio.deleteobjects import DeleteObject
5
4
  from minio import Minio
5
+ from minio.deleteobjects import DeleteObject
6
6
  except ImportError:
7
7
  raise ImportError('Please install minio or tracktolib with "s3-minio" to use this module')
8
8
 
tracktolib/s3/niquests.py CHANGED
@@ -1,18 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import namedtuple
4
- from pathlib import Path
5
-
3
+ import hashlib
6
4
  import http
7
5
  import xml.etree.ElementTree as ET
8
6
  from contextlib import asynccontextmanager
9
7
  from dataclasses import dataclass, field
10
- from typing import AsyncIterator, Callable, Literal, Self, TypedDict, Unpack
8
+ from pathlib import Path
9
+ from typing import AsyncIterator, Awaitable, Callable, Literal, NamedTuple, Required, Self, TypedDict, Unpack
10
+ from urllib.parse import unquote
11
+ from xml.sax.saxutils import escape as xml_escape
11
12
 
12
13
  try:
13
14
  import botocore.client
14
15
  import botocore.session
15
16
  import jmespath
17
+ from botocore.auth import SigV4Auth
18
+ from botocore.awsrequest import AWSRequest
16
19
  from botocore.config import Config
17
20
  except ImportError as e:
18
21
  raise ImportError("botocore is required for S3 operations. Install with tracktolib[s3-niquests]") from e
@@ -22,26 +25,37 @@ try:
22
25
  except ImportError as e:
23
26
  raise ImportError("niquests is required for S3 operations. Install with tracktolib[s3-niquests]") from e
24
27
 
28
+ try:
29
+ import ujson as json
30
+ except ImportError:
31
+ import json
32
+
25
33
  from ..utils import get_stream_chunk
26
34
 
27
35
  __all__ = (
28
- "S3Session",
29
- "s3_delete_object",
30
- "s3_delete_objects",
31
- "s3_list_files",
32
- "s3_put_object",
33
- "s3_get_object",
34
- "s3_download_file",
35
- "s3_upload_file",
36
- "s3_create_multipart_upload",
37
- "s3_multipart_upload",
38
- "s3_file_upload",
39
36
  "S3MultipartUpload",
40
37
  "S3Object",
41
38
  "S3ObjectParams",
39
+ "S3Session",
42
40
  "UploadPart",
43
41
  "build_s3_headers",
44
42
  "build_s3_presigned_params",
43
+ "s3_create_multipart_upload",
44
+ "s3_delete_bucket_policy",
45
+ "s3_delete_bucket_website",
46
+ "s3_delete_object",
47
+ "s3_delete_objects",
48
+ "s3_download_file",
49
+ "s3_empty_bucket",
50
+ "s3_file_upload",
51
+ "s3_get_bucket_policy",
52
+ "s3_get_object",
53
+ "s3_list_files",
54
+ "s3_multipart_upload",
55
+ "s3_put_bucket_policy",
56
+ "s3_put_bucket_website",
57
+ "s3_put_object",
58
+ "s3_upload_file",
45
59
  )
46
60
 
47
61
  ACL = Literal[
@@ -193,16 +207,16 @@ class S3Session:
193
207
  s3_config: Config | None = None
194
208
  s3_client: botocore.client.BaseClient | None = None
195
209
  http_client: niquests.AsyncSession = field(default_factory=niquests.AsyncSession)
210
+ _botocore_session: botocore.session.Session | None = field(default=None, init=False, repr=False)
196
211
 
197
212
  def __post_init__(self):
198
213
  if self.s3_client is None:
199
- session = botocore.session.Session()
200
- self.s3_client = session.create_client(
214
+ self._botocore_session = botocore.session.Session()
215
+ self._botocore_session.set_credentials(self.access_key, self.secret_key)
216
+ self.s3_client = self._botocore_session.create_client(
201
217
  "s3",
202
218
  endpoint_url=self.endpoint_url,
203
219
  region_name=self.region,
204
- aws_access_key_id=self.access_key,
205
- aws_secret_access_key=self.secret_key,
206
220
  config=self.s3_config,
207
221
  )
208
222
 
@@ -220,13 +234,13 @@ class S3Session:
220
234
  await self.http_client.__aexit__(exc_type, exc_val, exc_tb)
221
235
  self._s3.close()
222
236
 
223
- async def delete_object(self, bucket: str, key: str) -> niquests.Response:
237
+ async def delete_object(self, bucket: str, key: str) -> niquests.AsyncResponse:
224
238
  """Delete an object from S3."""
225
- return await s3_delete_object(self._s3, self.http_client, bucket, key)
239
+ return await s3_delete_object(self._s3, self.http_client, bucket, key) # pyright: ignore[reportReturnType]
226
240
 
227
- async def delete_objects(self, bucket: str, keys: list[str]) -> list[niquests.Response]:
241
+ async def delete_objects(self, bucket: str, keys: list[str]) -> list[niquests.AsyncResponse]:
228
242
  """Delete multiple objects from S3."""
229
- return await s3_delete_objects(self._s3, self.http_client, bucket, keys)
243
+ return await s3_delete_objects(self._s3, self.http_client, bucket, keys) # pyright: ignore[reportReturnType]
230
244
 
231
245
  def list_files(
232
246
  self,
@@ -252,15 +266,15 @@ class S3Session:
252
266
 
253
267
  async def put_object(
254
268
  self, bucket: str, key: str, data: bytes, **kwargs: Unpack[S3ObjectParams]
255
- ) -> niquests.Response:
269
+ ) -> niquests.AsyncResponse:
256
270
  """Upload an object to S3."""
257
- return await s3_put_object(self._s3, self.http_client, bucket, key, data, **kwargs)
271
+ return await s3_put_object(self._s3, self.http_client, bucket, key, data, **kwargs) # pyright: ignore[reportReturnType]
258
272
 
259
273
  async def upload_file(
260
274
  self, bucket: str, file: Path, path: str, **kwargs: Unpack[S3ObjectParams]
261
- ) -> niquests.Response:
275
+ ) -> niquests.AsyncResponse:
262
276
  """Upload a file to S3."""
263
- return await s3_upload_file(self._s3, self.http_client, bucket, file, path, **kwargs)
277
+ return await s3_upload_file(self._s3, self.http_client, bucket, file, path, **kwargs) # pyright: ignore[reportReturnType]
264
278
 
265
279
  async def get_object(self, bucket: str, key: str) -> bytes | None:
266
280
  """Download an object from S3."""
@@ -310,10 +324,33 @@ class S3Session:
310
324
  **kwargs,
311
325
  )
312
326
 
327
+ async def put_bucket_policy(self, bucket: str, policy: str | dict) -> niquests.AsyncResponse:
328
+ """Set a bucket policy."""
329
+ return await s3_put_bucket_policy(self._s3, self.http_client, bucket, policy, self._botocore_session) # pyright: ignore[reportReturnType]
313
330
 
314
- S3MultipartUpload = namedtuple(
315
- "S3MultipartUpload", ["fetch_create", "fetch_complete", "upload_part", "generate_presigned_url", "fetch_abort"]
316
- )
331
+ async def get_bucket_policy(self, bucket: str) -> dict | None:
332
+ """Get a bucket policy. Returns None if no policy exists."""
333
+ return await s3_get_bucket_policy(self._s3, self.http_client, bucket)
334
+
335
+ async def delete_bucket_policy(self, bucket: str) -> niquests.AsyncResponse:
336
+ """Delete a bucket policy."""
337
+ return await s3_delete_bucket_policy(self._s3, self.http_client, bucket) # pyright: ignore[reportReturnType]
338
+
339
+ async def put_bucket_website(
340
+ self, bucket: str, index_document: str = "index.html", error_document: str | None = None
341
+ ) -> niquests.AsyncResponse:
342
+ """Configure a bucket as a static website."""
343
+ return await s3_put_bucket_website(
344
+ self._s3, self.http_client, bucket, index_document, error_document, self._botocore_session
345
+ ) # pyright: ignore[reportReturnType]
346
+
347
+ async def delete_bucket_website(self, bucket: str) -> niquests.AsyncResponse:
348
+ """Remove website configuration from a bucket."""
349
+ return await s3_delete_bucket_website(self._s3, self.http_client, bucket) # pyright: ignore[reportReturnType]
350
+
351
+ async def empty_bucket(self, bucket: str, *, on_progress: Callable[[str], None] | None = None) -> int:
352
+ """Delete all objects from a bucket. Returns count of deleted objects."""
353
+ return await s3_empty_bucket(self._s3, self.http_client, bucket, on_progress=on_progress)
317
354
 
318
355
 
319
356
  class UploadPart(TypedDict):
@@ -321,8 +358,16 @@ class UploadPart(TypedDict):
321
358
  ETag: str | None
322
359
 
323
360
 
361
+ class S3MultipartUpload(NamedTuple):
362
+ fetch_create: Callable[[], Awaitable[str]]
363
+ fetch_complete: Callable[[], Awaitable[niquests.Response]]
364
+ upload_part: Callable[[bytes], Awaitable[UploadPart]]
365
+ generate_presigned_url: Callable[..., str]
366
+ fetch_abort: Callable[[], Awaitable[niquests.Response]]
367
+
368
+
324
369
  class S3Object(TypedDict, total=False):
325
- Key: str
370
+ Key: Required[str]
326
371
  LastModified: str
327
372
  ETag: str
328
373
  Size: int
@@ -398,12 +443,15 @@ async def s3_list_files(
398
443
 
399
444
  page_items: list[S3Object] = []
400
445
  for contents in root.findall("s3:Contents", ns):
401
- item: S3Object = {}
446
+ item: S3Object = {} # pyright: ignore[reportAssignmentType]
402
447
  for child in contents:
403
448
  tag = child.tag.replace(f"{{{ns['s3']}}}", "")
404
449
  item[tag] = child.text
405
450
  if "Size" in item:
406
451
  item["Size"] = int(item["Size"])
452
+ # URL-decode the Key (some S3-compatible servers like Garage return URL-encoded keys)
453
+ if "Key" in item and item["Key"]:
454
+ item["Key"] = unquote(item["Key"])
407
455
  page_items.append(item)
408
456
 
409
457
  if search_query:
@@ -676,3 +724,158 @@ async def s3_file_upload(
676
724
  return
677
725
  await mpart.upload_part(chunk)
678
726
  has_uploaded_parts = True
727
+
728
+
729
+ def _get_credentials(s3: botocore.client.BaseClient, session: botocore.session.Session | None = None):
730
+ """Get credentials from session (public API) or fall back to client internals."""
731
+ if session is not None:
732
+ return session.get_credentials()
733
+ # Fall back to private API for backward compatibility with standalone function usage
734
+ return s3._request_signer._credentials # pyright: ignore[reportAttributeAccessIssue]
735
+
736
+
737
+ def _sign_s3_request(
738
+ s3: botocore.client.BaseClient,
739
+ method: str,
740
+ url: str,
741
+ data: bytes,
742
+ content_type: str,
743
+ botocore_session: botocore.session.Session | None = None,
744
+ ) -> dict[str, str]:
745
+ """Create and sign an S3 request, returning the headers to use."""
746
+ request = AWSRequest(method=method, url=url, data=data, headers={"Content-Type": content_type})
747
+ request.headers["x-amz-content-sha256"] = hashlib.sha256(data).hexdigest()
748
+
749
+ credentials = _get_credentials(s3, botocore_session)
750
+ region = s3.meta.region_name
751
+ signer = SigV4Auth(credentials, "s3", region)
752
+ signer.add_auth(request)
753
+
754
+ return dict(request.headers)
755
+
756
+
757
+ async def s3_put_bucket_policy(
758
+ s3: botocore.client.BaseClient,
759
+ client: niquests.AsyncSession,
760
+ bucket: str,
761
+ policy: str | dict,
762
+ botocore_session: botocore.session.Session | None = None,
763
+ ) -> niquests.Response:
764
+ """
765
+ Set a bucket policy using a signed request.
766
+
767
+ The policy can be provided as a JSON string or a dict (which will be serialized).
768
+ If botocore_session is provided, credentials are retrieved via the public API;
769
+ otherwise falls back to the client's internal credentials.
770
+ """
771
+ policy_str = policy if isinstance(policy, str) else json.dumps(policy)
772
+ policy_bytes = policy_str.encode("utf-8")
773
+ url = f"{s3.meta.endpoint_url}/{bucket}?policy"
774
+ headers = _sign_s3_request(s3, "PUT", url, policy_bytes, "application/json", botocore_session)
775
+ return (await client.put(url, data=policy_bytes, headers=headers)).raise_for_status()
776
+
777
+
778
+ async def s3_get_bucket_policy(
779
+ s3: botocore.client.BaseClient,
780
+ client: niquests.AsyncSession,
781
+ bucket: str,
782
+ ) -> dict | None:
783
+ """
784
+ Get a bucket policy using presigned URL.
785
+
786
+ Returns the policy as a dict, or None if no policy exists.
787
+ """
788
+ url = s3.generate_presigned_url(
789
+ ClientMethod="get_bucket_policy",
790
+ Params={"Bucket": bucket},
791
+ )
792
+ resp = await client.get(url)
793
+ if resp.status_code == http.HTTPStatus.NOT_FOUND:
794
+ return None
795
+ # NoSuchBucketPolicy returns 404 on AWS, but some providers may return other codes
796
+ if resp.status_code == http.HTTPStatus.NO_CONTENT:
797
+ return None
798
+ resp.raise_for_status()
799
+ if resp.content is None:
800
+ return None
801
+ return json.loads(resp.content)
802
+
803
+
804
+ async def s3_delete_bucket_policy(
805
+ s3: botocore.client.BaseClient,
806
+ client: niquests.AsyncSession,
807
+ bucket: str,
808
+ ) -> niquests.Response:
809
+ """Delete a bucket policy using presigned URL."""
810
+ url = s3.generate_presigned_url(
811
+ ClientMethod="delete_bucket_policy",
812
+ Params={"Bucket": bucket},
813
+ )
814
+ return (await client.delete(url)).raise_for_status()
815
+
816
+
817
+ def _build_website_xml(index_document: str, error_document: str | None, api_version: str) -> str:
818
+ """Build XML payload for S3 website configuration."""
819
+ index_xml = f"<IndexDocument><Suffix>{xml_escape(index_document)}</Suffix></IndexDocument>"
820
+ error_xml = f"<ErrorDocument><Key>{xml_escape(error_document)}</Key></ErrorDocument>" if error_document else ""
821
+ return f'<WebsiteConfiguration xmlns="http://s3.amazonaws.com/doc/{api_version}/">{index_xml}{error_xml}</WebsiteConfiguration>'
822
+
823
+
824
+ async def s3_put_bucket_website(
825
+ s3: botocore.client.BaseClient,
826
+ client: niquests.AsyncSession,
827
+ bucket: str,
828
+ index_document: str = "index.html",
829
+ error_document: str | None = None,
830
+ botocore_session: botocore.session.Session | None = None,
831
+ ) -> niquests.AsyncResponse:
832
+ """
833
+ Configure a bucket as a static website using a signed request.
834
+
835
+ Note: This operation is not supported by MinIO.
836
+ If botocore_session is provided, credentials are retrieved via the public API;
837
+ otherwise falls back to the client's internal credentials.
838
+ """
839
+ api_version = s3.meta.service_model.api_version
840
+ xml_payload = _build_website_xml(index_document, error_document, api_version)
841
+ xml_bytes = xml_payload.encode("utf-8")
842
+ url = f"{s3.meta.endpoint_url}/{bucket}?website"
843
+ headers = _sign_s3_request(s3, "PUT", url, xml_bytes, "application/xml", botocore_session)
844
+ return (await client.put(url, data=xml_bytes, headers=headers)).raise_for_status() # pyright: ignore[reportReturnType]
845
+
846
+
847
+ async def s3_delete_bucket_website(
848
+ s3: botocore.client.BaseClient,
849
+ client: niquests.AsyncSession,
850
+ bucket: str,
851
+ ) -> niquests.Response:
852
+ """Remove website configuration from a bucket using presigned URL."""
853
+ url = s3.generate_presigned_url(
854
+ ClientMethod="delete_bucket_website",
855
+ Params={"Bucket": bucket},
856
+ )
857
+ return (await client.delete(url)).raise_for_status()
858
+
859
+
860
+ async def s3_empty_bucket(
861
+ s3: botocore.client.BaseClient,
862
+ client: niquests.AsyncSession,
863
+ bucket: str,
864
+ *,
865
+ on_progress: Callable[[str], None] | None = None,
866
+ ) -> int:
867
+ """
868
+ Delete all objects from a bucket.
869
+
870
+ The optional on_progress callback is called with each deleted key.
871
+ Returns the count of deleted objects.
872
+ """
873
+ deleted_count = 0
874
+ async for obj in s3_list_files(s3, client, bucket, ""):
875
+ key = obj.get("Key")
876
+ if key:
877
+ await s3_delete_object(s3, client, bucket, key)
878
+ deleted_count += 1
879
+ if on_progress:
880
+ on_progress(key)
881
+ return deleted_count
tracktolib/s3/s3.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import datetime as dt
2
2
  from io import BytesIO
3
3
  from pathlib import Path
4
- from typing import TypedDict, Literal, Callable
4
+ from typing import Callable, Literal, TypedDict
5
5
 
6
6
  try:
7
7
  from aiobotocore.client import AioBaseClient
tracktolib/utils.py CHANGED
@@ -8,10 +8,19 @@ import subprocess
8
8
  import sys
9
9
  from decimal import Decimal
10
10
  from ipaddress import IPv4Address, IPv6Address
11
- from types import ModuleType
12
11
  from pathlib import Path
13
- from typing import Coroutine, AsyncIterable, AsyncIterator, Iterable, Iterator, Literal, overload, Any, Callable
14
-
12
+ from types import ModuleType
13
+ from typing import (
14
+ Any,
15
+ AsyncIterable,
16
+ AsyncIterator,
17
+ Callable,
18
+ Coroutine,
19
+ Iterable,
20
+ Iterator,
21
+ Literal,
22
+ overload,
23
+ )
15
24
 
16
25
  type OnCmdUpdate = Callable[[str], None]
17
26
  type OnCmdDone = Callable[[str, str, int], None]