From 5ac3d74251c149425ecb449856b896fec71cb1ed Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Thu, 6 Mar 2025 15:06:55 -0500 Subject: [PATCH] Restore python type-only constructs for simplicity --- obstore/python/obstore/__get.py | 114 ------------------ obstore/python/obstore/__init__.py | 4 - obstore/python/obstore/__put.py | 56 --------- obstore/python/obstore/__sign.py | 14 --- .../{_attributes.py => _attributes.pyi} | 0 obstore/python/obstore/_get.pyi | 110 ++++++++++++++++- obstore/python/obstore/_obstore.pyi | 9 ++ obstore/python/obstore/_put.pyi | 55 ++++++++- obstore/python/obstore/_sign.pyi | 16 ++- tests/store/test_s3.py | 23 +++- 10 files changed, 203 insertions(+), 198 deletions(-) delete mode 100644 obstore/python/obstore/__get.py delete mode 100644 obstore/python/obstore/__put.py delete mode 100644 obstore/python/obstore/__sign.py rename obstore/python/obstore/{_attributes.py => _attributes.pyi} (100%) diff --git a/obstore/python/obstore/__get.py b/obstore/python/obstore/__get.py deleted file mode 100644 index 4f480c5b..00000000 --- a/obstore/python/obstore/__get.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Type hints for get requests, that are actually Python classes.""" - -from datetime import datetime -from typing import TypedDict - - -class OffsetRange(TypedDict): - """Request all bytes starting from a given byte offset.""" - - offset: int - """The byte offset for the offset range request.""" - - -class SuffixRange(TypedDict): - """Request up to the last `n` bytes.""" - - suffix: int - """The number of bytes from the suffix to request.""" - - -class GetOptions(TypedDict, total=False): - """Options for a get request. - - All options are optional. - """ - - if_match: str | None - """ - Request will succeed if the `ObjectMeta::e_tag` matches - otherwise returning [`PreconditionError`][obstore.exceptions.PreconditionError]. - - See - - Examples: - - ```text - If-Match: "xyzzy" - If-Match: "xyzzy", "r2d2xxxx", "c3piozzzz" - If-Match: * - ``` - """ - - if_none_match: str | None - """ - Request will succeed if the `ObjectMeta::e_tag` does not match - otherwise returning [`NotModifiedError`][obstore.exceptions.NotModifiedError]. - - See - - Examples: - - ```text - If-None-Match: "xyzzy" - If-None-Match: "xyzzy", "r2d2xxxx", "c3piozzzz" - If-None-Match: * - ``` - """ - - if_unmodified_since: datetime | None - """ - Request will succeed if the object has been modified since - - - """ - - if_modified_since: datetime | None - """ - Request will succeed if the object has not been modified since - otherwise returning [`PreconditionError`][obstore.exceptions.PreconditionError]. - - Some stores, such as S3, will only return `NotModified` for exact - timestamp matches, instead of for any timestamp greater than or equal. - - - """ - - range: tuple[int, int] | list[int] | OffsetRange | SuffixRange - """ - Request transfer of only the specified range of bytes - otherwise returning [`NotModifiedError`][obstore.exceptions.NotModifiedError]. - - The semantics of this tuple are: - - - `(int, int)`: Request a specific range of bytes `(start, end)`. - - If the given range is zero-length or starts after the end of the object, an - error will be returned. Additionally, if the range ends after the end of the - object, the entire remainder of the object will be returned. Otherwise, the - exact requested range will be returned. - - The `end` offset is _exclusive_. - - - `{"offset": int}`: Request all bytes starting from a given byte offset. - - This is equivalent to `bytes={int}-` as an HTTP header. - - - `{"suffix": int}`: Request the last `int` bytes. Note that here, `int` is _the - size of the request_, not the byte offset. This is equivalent to `bytes=-{int}` - as an HTTP header. - - - """ - - version: str | None - """ - Request a particular object version - """ - - head: bool - """ - Request transfer of no content - - - """ diff --git a/obstore/python/obstore/__init__.py b/obstore/python/obstore/__init__.py index 1fab4ecd..be9a26a1 100644 --- a/obstore/python/obstore/__init__.py +++ b/obstore/python/obstore/__init__.py @@ -1,9 +1,5 @@ from typing import TYPE_CHECKING -from .__get import GetOptions, OffsetRange, SuffixRange -from .__put import PutMode, PutResult, UpdateVersion -from .__sign import HTTP_METHOD -from ._attributes import Attribute, Attributes from ._obstore import * from ._obstore import ___version diff --git a/obstore/python/obstore/__put.py b/obstore/python/obstore/__put.py deleted file mode 100644 index b8aa663b..00000000 --- a/obstore/python/obstore/__put.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Literal, TypeAlias, TypedDict - - -class UpdateVersion(TypedDict, total=False): - """Uniquely identifies a version of an object to update. - - Stores will use differing combinations of `e_tag` and `version` to provide - conditional updates, and it is therefore recommended applications preserve both - """ - - e_tag: str | None - """The unique identifier for the newly created object. - - - """ - - version: str | None - """A version indicator for the newly created object.""" - - -PutMode: TypeAlias = Literal["create", "overwrite"] | UpdateVersion -"""Configure preconditions for the put operation - -There are three modes: - -- Overwrite: Perform an atomic write operation, overwriting any object present at the - provided path. -- Create: Perform an atomic write operation, returning - [`AlreadyExistsError`][obstore.exceptions.AlreadyExistsError] if an object already - exists at the provided path. -- Update: Perform an atomic write operation if the current version of the object matches - the provided [`UpdateVersion`][obstore.UpdateVersion], returning - [`PreconditionError`][obstore.exceptions.PreconditionError] otherwise. - -If a string is provided, it must be one of: - -- `"overwrite"` -- `"create"` - -If a `dict` is provided, it must meet the criteria of -[`UpdateVersion`][obstore.UpdateVersion]. -""" - - -class PutResult(TypedDict): - """Result for a put request.""" - - e_tag: str | None - """ - The unique identifier for the newly created object - - - """ - - version: str | None - """A version indicator for the newly created object.""" diff --git a/obstore/python/obstore/__sign.py b/obstore/python/obstore/__sign.py deleted file mode 100644 index 510d04fd..00000000 --- a/obstore/python/obstore/__sign.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Literal, TypeAlias - -HTTP_METHOD: TypeAlias = Literal[ - "GET", - "PUT", - "POST", - "HEAD", - "PATCH", - "TRACE", - "DELETE", - "OPTIONS", - "CONNECT", -] -"""Allowed HTTP Methods for signing.""" diff --git a/obstore/python/obstore/_attributes.py b/obstore/python/obstore/_attributes.pyi similarity index 100% rename from obstore/python/obstore/_attributes.py rename to obstore/python/obstore/_attributes.pyi diff --git a/obstore/python/obstore/_get.pyi b/obstore/python/obstore/_get.pyi index bf141e7e..799274f4 100644 --- a/obstore/python/obstore/_get.pyi +++ b/obstore/python/obstore/_get.pyi @@ -1,11 +1,119 @@ from collections.abc import Sequence +from datetime import datetime +from typing import TypedDict -from .__get import GetOptions from ._attributes import Attributes from ._bytes import Bytes from ._list import ObjectMeta from .store import ObjectStore +class OffsetRange(TypedDict): + """Request all bytes starting from a given byte offset.""" + + offset: int + """The byte offset for the offset range request.""" + +class SuffixRange(TypedDict): + """Request up to the last `n` bytes.""" + + suffix: int + """The number of bytes from the suffix to request.""" + +class GetOptions(TypedDict, total=False): + """Options for a get request. + + All options are optional. + """ + + if_match: str | None + """ + Request will succeed if the `ObjectMeta::e_tag` matches + otherwise returning [`PreconditionError`][obstore.exceptions.PreconditionError]. + + See + + Examples: + + ```text + If-Match: "xyzzy" + If-Match: "xyzzy", "r2d2xxxx", "c3piozzzz" + If-Match: * + ``` + """ + + if_none_match: str | None + """ + Request will succeed if the `ObjectMeta::e_tag` does not match + otherwise returning [`NotModifiedError`][obstore.exceptions.NotModifiedError]. + + See + + Examples: + + ```text + If-None-Match: "xyzzy" + If-None-Match: "xyzzy", "r2d2xxxx", "c3piozzzz" + If-None-Match: * + ``` + """ + + if_unmodified_since: datetime | None + """ + Request will succeed if the object has been modified since + + + """ + + if_modified_since: datetime | None + """ + Request will succeed if the object has not been modified since + otherwise returning [`PreconditionError`][obstore.exceptions.PreconditionError]. + + Some stores, such as S3, will only return `NotModified` for exact + timestamp matches, instead of for any timestamp greater than or equal. + + + """ + + range: tuple[int, int] | list[int] | OffsetRange | SuffixRange + """ + Request transfer of only the specified range of bytes + otherwise returning [`NotModifiedError`][obstore.exceptions.NotModifiedError]. + + The semantics of this tuple are: + + - `(int, int)`: Request a specific range of bytes `(start, end)`. + + If the given range is zero-length or starts after the end of the object, an + error will be returned. Additionally, if the range ends after the end of the + object, the entire remainder of the object will be returned. Otherwise, the + exact requested range will be returned. + + The `end` offset is _exclusive_. + + - `{"offset": int}`: Request all bytes starting from a given byte offset. + + This is equivalent to `bytes={int}-` as an HTTP header. + + - `{"suffix": int}`: Request the last `int` bytes. Note that here, `int` is _the + size of the request_, not the byte offset. This is equivalent to `bytes=-{int}` + as an HTTP header. + + + """ + + version: str | None + """ + Request a particular object version + """ + + head: bool + """ + Request transfer of no content + + + """ + class GetResult: """Result for a get request. diff --git a/obstore/python/obstore/_obstore.pyi b/obstore/python/obstore/_obstore.pyi index 7aa81ec9..e800ef8a 100644 --- a/obstore/python/obstore/_obstore.pyi +++ b/obstore/python/obstore/_obstore.pyi @@ -1,3 +1,5 @@ +from ._attributes import Attribute as Attribute +from ._attributes import Attributes as Attributes from ._buffered import AsyncReadableFile as AsyncReadableFile from ._buffered import AsyncWritableFile as AsyncWritableFile from ._buffered import ReadableFile as ReadableFile @@ -12,7 +14,10 @@ from ._copy import copy_async as copy_async from ._delete import delete as delete from ._delete import delete_async as delete_async from ._get import BytesStream as BytesStream +from ._get import GetOptions as GetOptions from ._get import GetResult as GetResult +from ._get import OffsetRange as OffsetRange +from ._get import SuffixRange as SuffixRange from ._get import get as get from ._get import get_async as get_async from ._get import get_range as get_range @@ -28,10 +33,14 @@ from ._list import ObjectMeta as ObjectMeta from ._list import list as list # noqa: A004 from ._list import list_with_delimiter as list_with_delimiter from ._list import list_with_delimiter_async as list_with_delimiter_async +from ._put import PutMode as PutMode +from ._put import PutResult as PutResult +from ._put import UpdateVersion as UpdateVersion from ._put import put as put from ._put import put_async as put_async from ._rename import rename as rename from ._rename import rename_async as rename_async +from ._sign import HTTP_METHOD as HTTP_METHOD from ._sign import SignCapableStore as SignCapableStore from ._sign import sign as sign from ._sign import sign_async as sign_async diff --git a/obstore/python/obstore/_put.pyi b/obstore/python/obstore/_put.pyi index dd86ef99..3f35139c 100644 --- a/obstore/python/obstore/_put.pyi +++ b/obstore/python/obstore/_put.pyi @@ -1,9 +1,8 @@ import sys from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator from pathlib import Path -from typing import IO +from typing import IO, Literal, TypeAlias, TypedDict -from .__put import PutMode, PutResult from ._attributes import Attributes from .store import ObjectStore @@ -12,6 +11,58 @@ if sys.version_info >= (3, 12): else: from typing_extensions import Buffer +class UpdateVersion(TypedDict, total=False): + """Uniquely identifies a version of an object to update. + + Stores will use differing combinations of `e_tag` and `version` to provide + conditional updates, and it is therefore recommended applications preserve both + """ + + e_tag: str | None + """The unique identifier for the newly created object. + + + """ + + version: str | None + """A version indicator for the newly created object.""" + +PutMode: TypeAlias = Literal["create", "overwrite"] | UpdateVersion +"""Configure preconditions for the put operation + +There are three modes: + +- Overwrite: Perform an atomic write operation, overwriting any object present at the + provided path. +- Create: Perform an atomic write operation, returning + [`AlreadyExistsError`][obstore.exceptions.AlreadyExistsError] if an object already + exists at the provided path. +- Update: Perform an atomic write operation if the current version of the object matches + the provided [`UpdateVersion`][obstore.UpdateVersion], returning + [`PreconditionError`][obstore.exceptions.PreconditionError] otherwise. + +If a string is provided, it must be one of: + +- `"overwrite"` +- `"create"` + +If a `dict` is provided, it must meet the criteria of +[`UpdateVersion`][obstore.UpdateVersion]. +""" + +class PutResult(TypedDict): + """Result for a put request.""" + + e_tag: str | None + """ + The unique identifier for the newly created object + + + """ + + version: str | None + """A version indicator for the newly created object.""" + def put( store: ObjectStore, path: str, diff --git a/obstore/python/obstore/_sign.pyi b/obstore/python/obstore/_sign.pyi index 364c07fc..e6d40892 100644 --- a/obstore/python/obstore/_sign.pyi +++ b/obstore/python/obstore/_sign.pyi @@ -1,10 +1,22 @@ from collections.abc import Sequence from datetime import timedelta -from typing import TypeAlias, overload +from typing import Literal, TypeAlias, overload -from .__sign import HTTP_METHOD from .store import AzureStore, GCSStore, S3Store +HTTP_METHOD: TypeAlias = Literal[ + "GET", + "PUT", + "POST", + "HEAD", + "PATCH", + "TRACE", + "DELETE", + "OPTIONS", + "CONNECT", +] +"""Allowed HTTP Methods for signing.""" + SignCapableStore: TypeAlias = AzureStore | GCSStore | S3Store """ObjectStore instances that are capable of signing.""" diff --git a/tests/store/test_s3.py b/tests/store/test_s3.py index 5e070df7..fbe47687 100644 --- a/tests/store/test_s3.py +++ b/tests/store/test_s3.py @@ -1,3 +1,5 @@ +# ruff: noqa: PGH003 + import pickle import pytest @@ -31,21 +33,32 @@ def test_error_overlapping_config_kwargs(): # Also raises for variations of the same parameter with pytest.raises(BaseError, match="Duplicate key"): - S3Store("bucket", config={"aws_skip_signature": True}, skip_signature=True) + S3Store( + "bucket", + config={"aws_skip_signature": True}, # type: ignore + skip_signature=True, + ) with pytest.raises(BaseError, match="Duplicate key"): - S3Store("bucket", config={"AWS_SKIP_SIGNATURE": True}, skip_signature=True) + S3Store( + "bucket", + config={"AWS_SKIP_SIGNATURE": True}, # type: ignore + skip_signature=True, + ) def test_overlapping_config_keys(): with pytest.raises(BaseError, match="Duplicate key"): - S3Store("bucket", config={"aws_skip_signature": True, "skip_signature": True}) + S3Store( + "bucket", + config={"aws_skip_signature": True, "skip_signature": True}, # type: ignore + ) with pytest.raises(BaseError, match="Duplicate key"): - S3Store("bucket", aws_skip_signature=True, skip_signature=True) + S3Store("bucket", aws_skip_signature=True, skip_signature=True) # type: ignore with pytest.raises(BaseError, match="Duplicate key"): - S3Store("bucket", AWS_SKIP_SIGNATURE=True, skip_signature=True) + S3Store("bucket", AWS_SKIP_SIGNATURE=True, skip_signature=True) # type: ignore @pytest.mark.asyncio