Skip to content

Commit 3fe3efd

Browse files
author
Harrison Unruh
authored
Pre-beta updates (#3)
* Rework errors, add integration tests * Pull out .replit change * Lint fix * Promote to 0.0.1 * Run tests during prerelease * Multi-language support * Relock * Rename to new package * Update installation for semaphore * Fix semaphore * Fix env * Final cut * Update readme
1 parent dfb462b commit 3fe3efd

26 files changed

Lines changed: 705 additions & 174 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
.pythonlibs
33
.pytest_cache
44
.ruff_cache
5+
.tox
56
.upm
67

78
# Python
9+
.local
810
__pycache__
911

1012
# Builds and Testing

.semaphore/semaphore.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ blocks:
3232
- make lint
3333
dependencies:
3434
- install deps
35-
- name: unit test
35+
- name: test-unit
3636
task:
3737
prologue:
3838
commands:

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,20 @@ lint:
1010
lint-fix:
1111
@poetry run ruff check src tests --fix
1212

13+
.PHONY: test-integration
14+
test-integration:
15+
@poetry run pytest --cov-report term-missing --cov=./src ./tests/integration
16+
17+
.PHONY: test-integration-multi-language
18+
test-integration-multi-language:
19+
@tox
20+
1321
.PHONY: test-unit
1422
test-unit:
1523
@poetry run pytest --cov-report term-missing --cov=./src ./tests/unit
1624

1725
.PHONY: prerelease
18-
prerelease:
26+
prerelease: test-unit test-integration-multi-language
1927
@rm -rf dist
2028
@poetry build
2129

README.md

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
1-
# replit-storage-python
2-
The library for Replit Object Storage. Development should "just work" on Replit!
1+
# replit-object-storage-python
2+
The library for interacting with Replit's Object Storage service, on Replit.
33

4-
## Development
4+
## Usage
55

6-
To get setup, run:
7-
```bash
8-
make install
6+
### Setup
7+
8+
Start by importing the Object Storage Client:
9+
```python
10+
from replit.object_storage import Client
911
```
1012

11-
To run the linter, run:
12-
```bash
13-
make lint
13+
Then to use the Client:
14+
```python
15+
client = Client()
1416
```
1517

16-
or to fix (fixable) lint issues, run:
17-
```bash
18-
make lint-fix
18+
### Downloading an Object
19+
20+
```python
21+
contents = client.download_as_text("file.json")
1922
```
2023

21-
To run tests, run:
22-
```bash
23-
make test
24+
### Uploading an Object
25+
26+
```python
27+
client.upload_from_text("file.json", data)
2428
```
2529

26-
## Release
30+
### List Objects
2731

28-
To check that the package builds, you can run:
29-
```bash
30-
make prerelease
32+
```python
33+
client.list()
3134
```
3235

33-
To perform a release, first bump the version in `pyproject.toml`. Then run:
34-
```bash
35-
make release
36-
```
36+
### Delete an Object
37+
38+
```python
39+
contents = client.delete("file.json")
40+
```

development.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# replit-object-storage-python
2+
The library for Replit Object Storage. Development should "just work" on Replit!
3+
4+
## Development
5+
6+
To get setup, run:
7+
```bash
8+
make install
9+
```
10+
11+
To run the linter, run:
12+
```bash
13+
make lint
14+
```
15+
16+
or to fix (fixable) lint issues, run:
17+
```bash
18+
make lint-fix
19+
```
20+
21+
To run tests, run:
22+
```bash
23+
make test
24+
```
25+
26+
## Release
27+
28+
To check that the package builds, you can run:
29+
```bash
30+
make prerelease
31+
```
32+
33+
To perform a release, first bump the version in `pyproject.toml`. Then run:
34+
```bash
35+
make release
36+
```

poetry.lock

Lines changed: 243 additions & 92 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
[tool.poetry]
2-
name = "replit.storage"
3-
version = "0.0.1.a3"
2+
name = "replit.object_storage"
3+
version = "0.0.2"
44
description = "A library for interacting with Object Storage on Replit"
55
authors = ["Repl.it <contact@repl.it>"]
66
license = "ISC"
77
readme = "README.md"
8-
repository = "https://github.com/replit/replit-storage-python"
9-
homepage = "https://github.com/replit/replit-storage-python"
8+
repository = "https://github.com/replit/replit-object-storage-python"
9+
homepage = "https://github.com/replit/replit-object-storage-python"
1010
classifiers = [
1111
"Programming Language :: Python :: 3",
1212
"License :: OSI Approved :: ISC License (ISCL)",
1313
"Operating System :: OS Independent",
1414
]
1515
packages = [
1616
{ include = "replit", from = "src" },
17-
{ include = "tests" }
1817
]
1918
exclude = ["tests"]
2019

2120
[tool.poetry.dependencies]
22-
python = ">=3.10.0,<3.11"
21+
python = ">=3.8.0,<3.13"
2322
google-cloud-storage = "^2.14.0"
2423
requests = "^2.31.0"
2524

@@ -28,6 +27,7 @@ ruff = "^0.1.15"
2827
pytest = "^8.0.0"
2928
twine = "*"
3029
pytest-cov = "^4.1.0"
30+
tox = "^4.13.0"
3131

3232
[tool.pyright]
3333
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md

replit.nix

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
{ pkgs }: {
2-
deps = [];
2+
deps = [
3+
pkgs.python38
4+
pkgs.python39
5+
pkgs.python311
6+
pkgs.python312
7+
];
38
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Public interface for the replit.object_storage library."""
2+
3+
from replit.object_storage.client import Client
4+
from replit.object_storage.errors import DefaultBucketError
5+
from replit.object_storage.object import Object
Lines changed: 81 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
many docstrings are borrowed from the underlying library.
55
"""
66

7-
from typing import Optional
7+
from typing import List, Optional
88

99
import requests
1010
from google.auth import identity_pool
1111
from google.cloud import storage
12-
from replit.storage.config import REPLIT_ADC, REPLIT_DEFAULT_BUCKET_URL
13-
from replit.storage.errors import DefaultBucketError
12+
from google.cloud.exceptions import NotFound
13+
from replit.object_storage.config import REPLIT_ADC, REPLIT_DEFAULT_BUCKET_URL
14+
from replit.object_storage.errors import (
15+
DefaultBucketError,
16+
ObjectNotFoundError,
17+
google_error_handler,
18+
)
19+
from replit.object_storage.object import Object
1420

1521

1622
class Client:
@@ -39,14 +45,41 @@ def __init__(self, bucket_id: Optional[str] = None):
3945
self.__gcs_client = storage.Client(credentials=creds, project="")
4046
self.__gcs_bucket_handle = None
4147

42-
def delete(self, object_name: str) -> None:
48+
@google_error_handler
49+
def copy(self, object_name: str, dest_object_name: str) -> None:
50+
"""Copies the specified object within the same bucket.
51+
52+
If an object exists in the same location, it will be overwritten.
53+
54+
Args:
55+
object_name: The full path of the object to be copied.
56+
dest_object_name: The full path to copy the object to.
57+
"""
58+
source_object = self.__object(object_name)
59+
bucket = self.__bucket()
60+
bucket.copy_blob(
61+
source_object,
62+
bucket,
63+
dest_object_name,
64+
)
65+
66+
@google_error_handler
67+
def delete(self, object_name: str, ignore_not_found: bool = False) -> None:
4368
"""Deletes an object from Object Storage.
4469
4570
Args:
4671
object_name: The name of the object to be deleted.
72+
ignore_not_found: Whether an error should be raised if the object does not
73+
exist.
4774
"""
48-
return self.__object(object_name).delete()
75+
try:
76+
return self.__object(object_name).delete()
77+
except NotFound as err:
78+
if ignore_not_found:
79+
return
80+
raise ObjectNotFoundError("The requested object could not be found.") from err
4981

82+
@google_error_handler
5083
def download_as_bytes(self, object_name: str) -> bytes:
5184
"""Download the contents an object as a bytes object.
5285
@@ -55,23 +88,16 @@ def download_as_bytes(self, object_name: str) -> bytes:
5588
"""
5689
return self.__object(object_name).download_as_bytes()
5790

58-
def download_as_string(self, object_name: str) -> str:
91+
@google_error_handler
92+
def download_as_text(self, object_name: str) -> str:
5993
"""Download the contents an object as a string.
6094
6195
Args:
6296
object_name: The name of the object to be downloaded.
6397
"""
6498
return self.__object(object_name).download_as_text()
6599

66-
def download_to_file(self, object_name: str, dest_file) -> None:
67-
"""Download the contents an object into a file-like object.
68-
69-
Args:
70-
object_name: The name of the object to be downloaded.
71-
dest_file: A file-like object.
72-
"""
73-
return self.__object(object_name).download_to_file(dest_file)
74-
100+
@google_error_handler
75101
def download_to_filename(self, object_name: str, dest_filename: str) -> None:
76102
"""Download the contents an object into a file on the local disk.
77103
@@ -81,6 +107,7 @@ def download_to_filename(self, object_name: str, dest_filename: str) -> None:
81107
"""
82108
return self.__object(object_name).download_to_filename(dest_filename)
83109

110+
@google_error_handler
84111
def exists(self, object_name: str) -> bool:
85112
"""Checks if an object exist.
86113
@@ -89,15 +116,42 @@ def exists(self, object_name: str) -> bool:
89116
"""
90117
return self.__object(object_name).exists()
91118

92-
def upload_from_file(self, dest_object_name: str, src_file) -> None:
93-
"""Uploads the contents of a file-like object.
119+
@google_error_handler
120+
def list(
121+
self,
122+
end_offset: Optional[str] = None,
123+
match_glob: Optional[str] = None,
124+
max_results: Optional[int] = None,
125+
prefix: Optional[str] = None,
126+
start_offset: Optional[str] = None,
127+
) -> List[Object]:
128+
"""Lists objects in the bucket.
94129
95130
Args:
96-
dest_object_name: The name of the object to be uploaded.
97-
src_file: A file-like object.
131+
end_offset: Filter results to objects whose names are lexicographically
132+
before end_offset. If start_offset is also set, the objects listed
133+
have names between start_offset (inclusive) and end_offset
134+
(exclusive).
135+
match_glob: Glob pattern used to filter results, for example foo*bar.
136+
max_results: The maximum number of results that can be returned in the
137+
response.
138+
prefix: Filter results to objects who names have the specified prefix.
139+
start_offset: Filter results to objects whose names are
140+
lexicographically equal to or after start_offset. If endOffset is
141+
also set, the objects listed have names between start_offset
142+
(inclusive) and end_offset (exclusive).
143+
98144
"""
99-
self.__object(dest_object_name).upload_from_file(src_file)
100-
145+
iter = self.__bucket().list_blobs(
146+
end_offset=end_offset,
147+
match_glob=match_glob,
148+
max_results=max_results,
149+
prefix=prefix,
150+
start_offset=start_offset,
151+
)
152+
return [Object(name=object.name) for object in iter]
153+
154+
@google_error_handler
101155
def upload_from_filename(self, dest_object_name: str,
102156
src_filename: str) -> None:
103157
"""Upload an object from a file on the local disk.
@@ -108,7 +162,8 @@ def upload_from_filename(self, dest_object_name: str,
108162
"""
109163
self.__object(dest_object_name).upload_from_filename(src_filename)
110164

111-
def upload_from_string(self, dest_object_name: str, src_data: str) -> None:
165+
@google_error_handler
166+
def upload_from_text(self, dest_object_name: str, src_data: str) -> None:
112167
"""Upload an object from a string.
113168
114169
Args:
@@ -117,11 +172,13 @@ def upload_from_string(self, dest_object_name: str, src_data: str) -> None:
117172
"""
118173
self.__object(dest_object_name).upload_from_string(src_data)
119174

120-
def __object(self, object_name: str) -> storage.Blob:
175+
def __bucket(self) -> storage.Bucket:
121176
if self.__gcs_bucket_handle is None:
122177
self.__gcs_bucket_handle = self.__get_bucket_handle()
178+
return self.__gcs_bucket_handle
123179

124-
return self.__gcs_bucket_handle.blob(object_name)
180+
def __object(self, object_name: str) -> storage.Blob:
181+
return self.__bucket().blob(object_name)
125182

126183
def __get_bucket_handle(self) -> storage.Bucket:
127184
if self.__bucket_id is None:

0 commit comments

Comments
 (0)