fallbacks3 0.1.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.
- fallbacks3/__init__.py +7 -0
- fallbacks3/config.py +11 -0
- fallbacks3/provider/__init__.py +3 -0
- fallbacks3/provider/config_credentials.py +12 -0
- fallbacks3/provider/provider.py +81 -0
- fallbacks3/storage.py +178 -0
- fallbacks3/utils/__init__.py +0 -0
- fallbacks3/utils/uri.py +51 -0
- fallbacks3-0.1.0.dist-info/METADATA +172 -0
- fallbacks3-0.1.0.dist-info/RECORD +12 -0
- fallbacks3-0.1.0.dist-info/WHEEL +5 -0
- fallbacks3-0.1.0.dist-info/top_level.txt +1 -0
fallbacks3/__init__.py
ADDED
fallbacks3/config.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProviderCredentials(BaseModel):
|
|
5
|
+
"""Parsed provider URI in the format: <provider>://<access_key>:<secret_key>@<endpoint>."""
|
|
6
|
+
|
|
7
|
+
provider_scheme: str
|
|
8
|
+
access_key: str
|
|
9
|
+
secret_key: str
|
|
10
|
+
endpoint: str
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(frozen=True)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from boto3 import client
|
|
2
|
+
from botocore.client import Config as BotoConfig
|
|
3
|
+
from mypy_boto3_s3.client import S3Client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class S3Provider:
|
|
7
|
+
"""S3-compatible storage provider."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
provider_scheme: str,
|
|
12
|
+
endpoint: str,
|
|
13
|
+
access_key_id: str,
|
|
14
|
+
secret_access_key: str,
|
|
15
|
+
):
|
|
16
|
+
"""Initialize S3 provider with credentials.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
provider_scheme: URI scheme for the provider (e.g., 'ps3', 'r2')
|
|
20
|
+
endpoint: S3 endpoint URL with scheme (e.g., 'https://ps3.palver.com')
|
|
21
|
+
access_key_id: Access key ID
|
|
22
|
+
secret_access_key: Secret access key
|
|
23
|
+
"""
|
|
24
|
+
self.provider_scheme: str = provider_scheme
|
|
25
|
+
self.endpoint: str = endpoint
|
|
26
|
+
self.client: S3Client = client(
|
|
27
|
+
"s3",
|
|
28
|
+
config=BotoConfig(signature_version="s3v4"),
|
|
29
|
+
endpoint_url=endpoint,
|
|
30
|
+
aws_access_key_id=access_key_id,
|
|
31
|
+
aws_secret_access_key=secret_access_key,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def upload_file(self, file_name: str, file_path: str, bucket: str) -> str:
|
|
35
|
+
"""Upload a file to S3 bucket.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_name: Name/key for the file in the bucket
|
|
39
|
+
file_path: Local path to the file to upload
|
|
40
|
+
bucket: Bucket name
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Full URI of the uploaded file in format:
|
|
44
|
+
<provider_scheme>://<bucket>/<file_name>
|
|
45
|
+
"""
|
|
46
|
+
self.client.upload_file(file_path, bucket, file_name)
|
|
47
|
+
return f"{self.provider_scheme}://{bucket}/{file_name}"
|
|
48
|
+
|
|
49
|
+
def download_file(self, file_name: str, local_path: str, bucket: str) -> str:
|
|
50
|
+
"""Download a file from S3 bucket.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
file_name: Name/key of the file in the bucket
|
|
54
|
+
local_path: Local path where the file will be saved
|
|
55
|
+
bucket: Bucket name
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Local path where the file was saved
|
|
59
|
+
"""
|
|
60
|
+
self.client.download_file(bucket, file_name, local_path)
|
|
61
|
+
return local_path
|
|
62
|
+
|
|
63
|
+
def generate_signed_url(
|
|
64
|
+
self, file_name: str, bucket: str, expiration: int = 60
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Generate a presigned URL for temporary access to a file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
file_name: Name/key of the file in the bucket
|
|
70
|
+
bucket: Bucket name
|
|
71
|
+
expiration: Time in seconds for the URL to remain valid (default: 3600)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Presigned URL string
|
|
75
|
+
"""
|
|
76
|
+
url: str = self.client.generate_presigned_url(
|
|
77
|
+
"get_object",
|
|
78
|
+
Params={"Bucket": bucket, "Key": file_name},
|
|
79
|
+
ExpiresIn=expiration,
|
|
80
|
+
)
|
|
81
|
+
return url
|
fallbacks3/storage.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from fallbacks3.config import config
|
|
2
|
+
from fallbacks3.provider import S3Provider
|
|
3
|
+
from fallbacks3.utils.uri import build_provider_credentials, parse_uri
|
|
4
|
+
from typing import Dict, Tuple
|
|
5
|
+
from urllib.parse import ParseResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Storage:
|
|
9
|
+
"""S3-compatible storage client with multi-provider support and automatic fallback."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
providers: str = config.providers,
|
|
14
|
+
fallback_provider: str = config.fallback_provider,
|
|
15
|
+
):
|
|
16
|
+
"""Initialize storage with multiple providers.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
providers: Comma-separated provider URIs in format:
|
|
20
|
+
<provider>://<access_key>:<secret_key>@<endpoint>
|
|
21
|
+
Example: "ps3://key:secret@ps3.palver.com,s4://a:b@s4.mega.com"
|
|
22
|
+
Defaults to PROVIDERS environment variable.
|
|
23
|
+
fallback_provider: Provider scheme to use for fallback (e.g., "r2").
|
|
24
|
+
Must be one of the providers in the providers string.
|
|
25
|
+
Defaults to FALLBACK_PROVIDER environment variable.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If fallback_provider not found in providers
|
|
29
|
+
"""
|
|
30
|
+
self.providers: Dict[str, S3Provider] = {}
|
|
31
|
+
|
|
32
|
+
# Parse and initialize all providers
|
|
33
|
+
for provider in providers.split(","):
|
|
34
|
+
stripped_uri: str = provider.strip()
|
|
35
|
+
if stripped_uri:
|
|
36
|
+
creds = build_provider_credentials(uri=stripped_uri)
|
|
37
|
+
self.providers[creds.provider_scheme] = S3Provider(
|
|
38
|
+
provider_scheme=creds.provider_scheme,
|
|
39
|
+
endpoint=creds.endpoint,
|
|
40
|
+
access_key_id=creds.access_key,
|
|
41
|
+
secret_access_key=creds.secret_key,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.fallback_provider: str = fallback_provider
|
|
45
|
+
|
|
46
|
+
if fallback_provider not in self.providers:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Fallback provider '{fallback_provider}' not found in providers"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def upload_file(self, remote_file_uri: str, local_file_path: str) -> str:
|
|
52
|
+
"""Upload a file with automatic fallback on failure.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
remote_file_uri: Remote file URI in format <provider>://<bucket>/<file_path>
|
|
56
|
+
Example: "ps3://palver-whatsapp/file.mp4"
|
|
57
|
+
local_file_path: Local path to the file to upload
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
URI of uploaded file in format: <provider>://<bucket>/<file_path>
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If provider is not configured
|
|
64
|
+
Exception: If upload fails on both primary and fallback providers
|
|
65
|
+
"""
|
|
66
|
+
parsed_uri, provider = self._resolve_uri(uri=remote_file_uri)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
return provider.upload_file(
|
|
70
|
+
file_name=parsed_uri.path.lstrip("/"),
|
|
71
|
+
file_path=local_file_path,
|
|
72
|
+
bucket=parsed_uri.netloc,
|
|
73
|
+
)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
# Try fallback if different from primary
|
|
76
|
+
if self.fallback_provider != parsed_uri.scheme:
|
|
77
|
+
try:
|
|
78
|
+
fallback: S3Provider = self.providers[self.fallback_provider]
|
|
79
|
+
# Use same bucket and key with fallback provider
|
|
80
|
+
return fallback.upload_file(
|
|
81
|
+
file_name=parsed_uri.path.lstrip("/"),
|
|
82
|
+
file_path=local_file_path,
|
|
83
|
+
bucket=parsed_uri.netloc,
|
|
84
|
+
)
|
|
85
|
+
except Exception as fallback_error:
|
|
86
|
+
raise Exception(
|
|
87
|
+
f"Both primary and fallback providers failed. "
|
|
88
|
+
f"Primary error: {e}. Fallback error: {fallback_error}"
|
|
89
|
+
)
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
def download_file(self, remote_file_uri: str, local_file_path: str) -> str:
|
|
93
|
+
"""Download a file from storage.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
remote_file_uri: Remote file URI in format <provider>://<bucket>/<file_path>
|
|
97
|
+
Example: "ps3://palver-whatsapp/file.mp4"
|
|
98
|
+
local_file_path: Local path where file will be saved
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Local path where file was saved
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If provider is not configured
|
|
105
|
+
"""
|
|
106
|
+
parsed_uri, provider = self._resolve_uri(uri=remote_file_uri)
|
|
107
|
+
|
|
108
|
+
return provider.download_file(
|
|
109
|
+
file_name=parsed_uri.path.lstrip("/"),
|
|
110
|
+
local_path=local_file_path,
|
|
111
|
+
bucket=parsed_uri.netloc,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def generate_signed_url(self, remote_file_uri: str, expiration: int = 60) -> str:
|
|
115
|
+
"""Generate a presigned URL for temporary access.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
remote_file_uri: Remote file URI in format <provider>://<bucket>/<file_path>
|
|
119
|
+
Example: "ps3://palver-whatsapp/file.mp4"
|
|
120
|
+
expiration: Time in seconds for URL to remain valid (default: 60)
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Presigned URL string
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If provider is not configured
|
|
127
|
+
"""
|
|
128
|
+
parsed_uri, provider = self._resolve_uri(uri=remote_file_uri)
|
|
129
|
+
|
|
130
|
+
return provider.generate_signed_url(
|
|
131
|
+
file_name=parsed_uri.path.lstrip("/"),
|
|
132
|
+
bucket=parsed_uri.netloc,
|
|
133
|
+
expiration=expiration,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _resolve_uri(self, uri: str) -> Tuple[ParseResult, S3Provider]:
|
|
137
|
+
"""Parse and validate URI, then return parsed URI and provider.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
uri: Remote file URI in format <provider>://<bucket>/<file_path>
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Tuple of (parsed_uri, provider)
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If URI format is invalid or provider not configured
|
|
147
|
+
"""
|
|
148
|
+
parsed_uri: ParseResult = parse_uri(uri=uri)
|
|
149
|
+
self._validate_parsed_uri(parsed_uri=parsed_uri, remote_file_uri=uri)
|
|
150
|
+
provider: S3Provider = self.providers[parsed_uri.scheme]
|
|
151
|
+
return parsed_uri, provider
|
|
152
|
+
|
|
153
|
+
def _validate_parsed_uri(
|
|
154
|
+
self, parsed_uri: ParseResult, remote_file_uri: str
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Validate parsed URI format and provider configuration.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
parsed_uri: ParseResult from urlparse
|
|
160
|
+
remote_file_uri: Original URI string for error messages
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: If URI format is invalid or provider not configured
|
|
164
|
+
"""
|
|
165
|
+
if (
|
|
166
|
+
not parsed_uri.scheme
|
|
167
|
+
or not parsed_uri.netloc
|
|
168
|
+
or not parsed_uri.path.lstrip("/")
|
|
169
|
+
):
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"Invalid media path format: {remote_file_uri}. "
|
|
172
|
+
f"Expected format: <provider>://<bucket>/<file_path>"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if parsed_uri.scheme not in self.providers:
|
|
176
|
+
raise ValueError(f"Provider '{parsed_uri.scheme}' not configured")
|
|
177
|
+
|
|
178
|
+
return
|
|
File without changes
|
fallbacks3/utils/uri.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from fallbacks3.provider.config_credentials import ProviderCredentials
|
|
2
|
+
from urllib.parse import urlparse, ParseResult
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_uri(uri: str) -> ParseResult:
|
|
6
|
+
"""
|
|
7
|
+
Parse provider URI format: <provider_name>://<access_key>:<secret_key>@<endpoint>
|
|
8
|
+
|
|
9
|
+
Example: ps3://key:secret@ps3.palver.com
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
uri: Provider URI string
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
ParseResult with the following fields:
|
|
16
|
+
- scheme: Provider name (e.g., "ps3", "s4", "r2")
|
|
17
|
+
- username: Access key
|
|
18
|
+
- password: Secret key
|
|
19
|
+
- hostname: Endpoint hostname
|
|
20
|
+
- port: Endpoint port (None if not specified)
|
|
21
|
+
"""
|
|
22
|
+
return urlparse(uri)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_provider_credentials(uri: str) -> ProviderCredentials:
|
|
26
|
+
"""
|
|
27
|
+
Build provider credentials from URI.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
uri: Provider URI string in format <provider_name>://<access_key>:<secret_key>@<endpoint>
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
ProviderCredentials with parsed fields
|
|
34
|
+
"""
|
|
35
|
+
uri_parsed: ParseResult = parse_uri(uri=uri)
|
|
36
|
+
|
|
37
|
+
scheme: str = (
|
|
38
|
+
"http" if uri_parsed.hostname in ("localhost", "127.0.0.1") else "https"
|
|
39
|
+
)
|
|
40
|
+
endpoint = (
|
|
41
|
+
f"{scheme}://{uri_parsed.hostname}"
|
|
42
|
+
if not uri_parsed.port
|
|
43
|
+
else f"{scheme}://{uri_parsed.hostname}:{uri_parsed.port}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return ProviderCredentials(
|
|
47
|
+
provider_scheme=uri_parsed.scheme,
|
|
48
|
+
access_key=uri_parsed.username,
|
|
49
|
+
secret_key=uri_parsed.password,
|
|
50
|
+
endpoint=endpoint,
|
|
51
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fallbacks3
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: S3-compatible storage library with automatic fallback support
|
|
5
|
+
Author-email: Mila de Oliveira <mila.oliveira@palver.com.br>
|
|
6
|
+
Project-URL: Homepage, https://github.com/palverdata/fallbacks3
|
|
7
|
+
Project-URL: Repository, https://github.com/palverdata/fallbacks3
|
|
8
|
+
Project-URL: Issues, https://github.com/palverdata/fallbacks3/issues
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: boto3>=1.26.0
|
|
12
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
16
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
18
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
19
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: boto3-stubs[s3]>=1.34.0; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# fallbacks3
|
|
23
|
+
|
|
24
|
+
S3-compatible storage library with automatic fallback for the upload process.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install fallbacks3
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For development:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
You must configure a comma-separated list of providers, as well as the scheme of the fallback provider, with environment variables named `PROVIDERS` and `FALLBACK_PROVIDER`.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Comma-separated list of provider URIs
|
|
46
|
+
PROVIDERS="ps3://access_key:secret_key@ps3.palver.com,r2://access_key:secret_key@account.r2.cloudflarestorage.com"
|
|
47
|
+
|
|
48
|
+
# Fallback provider scheme (must be one of the providers in PROVIDERS)
|
|
49
|
+
FALLBACK_PROVIDER="r2"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Provider URI Format
|
|
54
|
+
|
|
55
|
+
Provider URIs follow the format:
|
|
56
|
+
```
|
|
57
|
+
<provider_scheme>://<access_key>:<secret_key>@<endpoint>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Examples: `r2://key:secret@account.r2.cloudflarestorage.com`, `local://test:testkey@localhost:9000`
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Remote File URI Format
|
|
64
|
+
|
|
65
|
+
File URIs should specify the exact location from a provider bucket:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
<provider>://<bucket>/<file_path>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Examples: `ps3://palver-whatsapp/audio.mp3`, `s3://my-bucket/documents/report.pdf`
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fallbacks3 import Storage
|
|
77
|
+
|
|
78
|
+
# Initialize storage using environment variables (PROVIDERS and FALLBACK_PROVIDER)
|
|
79
|
+
storage = Storage()
|
|
80
|
+
|
|
81
|
+
# Upload a local file to the provided remote path
|
|
82
|
+
stored_file_uri = storage.upload_file(
|
|
83
|
+
remote_file_uri="ps3://palver-whatsapp/audio.mp3",
|
|
84
|
+
local_file_path="/local/path/audio.mp3"
|
|
85
|
+
)
|
|
86
|
+
print(uri) # "ps3://palver-whatsapp/audio.mp3"
|
|
87
|
+
|
|
88
|
+
# Download a file
|
|
89
|
+
downloaded_file_path = storage.download_file(
|
|
90
|
+
remote_file_uri="ps3://palver-whatsapp/audio.mp3",
|
|
91
|
+
local_file_path="/local/path/audio.mp3"
|
|
92
|
+
)
|
|
93
|
+
print(local_path) # "/local/path/audio.mp3"
|
|
94
|
+
|
|
95
|
+
# Generate a signed URL for temporary access (default: 60 seconds)
|
|
96
|
+
signed_url = storage.generate_signed_url(
|
|
97
|
+
remote_file_uri="ps3://palver-whatsapp/audio.mp3",
|
|
98
|
+
expiration=3600
|
|
99
|
+
)
|
|
100
|
+
print(signed_url) # "https://ps3.palver.com/palver-whatsapp/audio.mp3?signature=..."
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Upload with Automatic Fallback
|
|
104
|
+
|
|
105
|
+
If the primary provider fails for the upload method, the library automatically retries with the fallback provider (using the same bucket and file path), as to ensure files are not lost.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# If ps3 fails, automatically falls back to r2
|
|
109
|
+
stored_file_path = storage.upload_file(
|
|
110
|
+
remote_file_uri="ps3://palver-whatsapp/audio.mp3",
|
|
111
|
+
local_file_path="/local/path/audio.mp3"
|
|
112
|
+
)
|
|
113
|
+
# Returns "r2://palver-whatsapp/audio.mp3" if ps3 failed
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Raises an exception if both primary and fallback providers fail.
|
|
117
|
+
|
|
118
|
+
### Download and Signed URLs
|
|
119
|
+
|
|
120
|
+
Download and signed URL generation use the provider specified in the remote file URI.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
Install development dependencies:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install -e ".[dev]"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
You can also develop with `uv` if available.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
uv pip install -e ".[dev]"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Run tests:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pytest --cov=fallbacks3
|
|
141
|
+
|
|
142
|
+
# alternatively, with uv:
|
|
143
|
+
uv run pytest --cov=fallbacks3
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Publishing to PyPI
|
|
147
|
+
|
|
148
|
+
The project is set up to be automatically uploaded to PyPI when a new tag is pushed. For a successful push, all tests must pass and the project must pass the `check` and `format` requirements.
|
|
149
|
+
|
|
150
|
+
### One-time setup
|
|
151
|
+
|
|
152
|
+
Before publishing for the first time:
|
|
153
|
+
|
|
154
|
+
1. **Configure Trusted Publishing on PyPI**:
|
|
155
|
+
- Go to https://pypi.org/manage/account/publishing/
|
|
156
|
+
- Add a new pending publisher:
|
|
157
|
+
- **PyPI Project Name**: `fallbacks3`
|
|
158
|
+
- **Owner**: `palverdata`
|
|
159
|
+
- **Repository name**: `fallbacks3`
|
|
160
|
+
- **Workflow name**: `publish.yaml`
|
|
161
|
+
- **Environment name**: `pypi`
|
|
162
|
+
|
|
163
|
+
You must have access to the `pypi` GitHub environment or be approved by required reviewers (`milasd` or `giancarlopro`) to publish.
|
|
164
|
+
|
|
165
|
+
### Publishing a new version
|
|
166
|
+
|
|
167
|
+
1. Update version in `pyproject.toml`
|
|
168
|
+
2. Create and push a tag:
|
|
169
|
+
```bash
|
|
170
|
+
git tag v[new version] # eg.: git tag v0.1.0
|
|
171
|
+
git push origin v[new version] # eg.: git push origin v0.1.0
|
|
172
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
fallbacks3/__init__.py,sha256=e8KpdMS8mWubH6QFn2_TGZgrGZtpZjil43vxzO-j_YM,186
|
|
2
|
+
fallbacks3/config.py,sha256=BIlekhzOkEVdqSmETpS1Y4Y8FpcJdwrdjnISnXVCevo,220
|
|
3
|
+
fallbacks3/storage.py,sha256=RicumVKzyJgsvpDL1u4yp2NiI1a47DLHNSVKkWwT_q0,6709
|
|
4
|
+
fallbacks3/provider/__init__.py,sha256=lFphOB3APdNzwQoJP_GgakL0ELXduBViFqoBBb3AR_0,78
|
|
5
|
+
fallbacks3/provider/config_credentials.py,sha256=cLvA4GVC9JYoJMBi17lbeibBGhf6exD7nCg-j0aEMXg,307
|
|
6
|
+
fallbacks3/provider/provider.py,sha256=V49uJht5m2o3qjOKVh4tjgUfpVDDEKiTkJcsbUhxNqY,2644
|
|
7
|
+
fallbacks3/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fallbacks3/utils/uri.py,sha256=8ur7zPs-ZdZBb-rZA9oG0_fo7CzMoOp9HnlAVMcV2sI,1475
|
|
9
|
+
fallbacks3-0.1.0.dist-info/METADATA,sha256=Z70wTuSDu8GSXUOLg5TE8XOsP_E-dYv8IW52GoAPo9A,4751
|
|
10
|
+
fallbacks3-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
fallbacks3-0.1.0.dist-info/top_level.txt,sha256=JsC-ee_dPAtcp_-4LvPVZk6KUAtrj8HQNWQzbxF9myU,11
|
|
12
|
+
fallbacks3-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fallbacks3
|