Skip to content

Commit 5cd9fc5

Browse files
author
Harrison Unruh
authored
Hello World! (#1)
* First alpha * Setup semaphore * Test CI breakage * Resolve issues * Feedback, make tests unit tests
1 parent 518498f commit 5cd9fc5

File tree

15 files changed

+1577
-2
lines changed

15 files changed

+1577
-2
lines changed

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Packager
2+
.pythonlibs
3+
.pytest_cache
4+
.ruff_cache
5+
.upm
6+
7+
# Python
8+
__pycache__
9+
10+
# Builds and Testing
11+
.coverage
12+
dist

.replit

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
entrypoint = "main.py"
2+
modules = ["python-3.10:v18-20230807-322e88b"]
3+
4+
hidden = [".pythonlibs"]
5+
6+
[nix]
7+
channel = "stable-23_05"
8+
9+
[unitTest]
10+
language = "python3"
11+
12+
[deployment]
13+
run = ["python3", "main.py"]
14+
deploymentTarget = "cloudrun"

.semaphore/semaphore.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
version: v1.0
2+
name: replit-storage-python
3+
agent:
4+
machine:
5+
type: e1-standard-4
6+
os_image: ubuntu2004
7+
blocks:
8+
- name: install deps
9+
task:
10+
jobs:
11+
- name: cache deps
12+
commands:
13+
- sem-version python 3.10
14+
- checkout --use-cache
15+
- pip install poetry==1.5.1
16+
- make install
17+
- cache store
18+
dependencies: []
19+
- name: lint
20+
task:
21+
prologue:
22+
commands:
23+
- sem-version python 3.10
24+
- checkout --use-cache
25+
- git switch -c pr
26+
- cache restore
27+
- pip install poetry==1.5.1
28+
- make install
29+
jobs:
30+
- name: make lint
31+
commands:
32+
- make lint
33+
dependencies:
34+
- install deps
35+
- name: unit test
36+
task:
37+
prologue:
38+
commands:
39+
- sem-version python 3.10
40+
- checkout --use-cache
41+
- git switch -c pr
42+
- cache restore
43+
- pip install poetry==1.5.1
44+
- make install
45+
jobs:
46+
- name: make test-unit
47+
commands:
48+
- make test-unit
49+
dependencies:
50+
- install deps

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.PHONY: install
2+
install:
3+
@poetry install
4+
5+
.PHONY: lint
6+
lint:
7+
@poetry run ruff check src tests
8+
9+
.PHONY: lint-fix
10+
lint-fix:
11+
@poetry run ruff check src tests --fix
12+
13+
.PHONY: test-unit
14+
test-unit:
15+
@poetry run pytest --cov-report term-missing --cov=./src ./tests/unit
16+
17+
.PHONY: prerelease
18+
prerelease:
19+
@rm -rf dist
20+
@poetry build
21+
22+
.PHONY: release
23+
release: prerelease
24+
@poetry run twine upload dist/*

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,36 @@
1-
# replit-objectstorage-python
2-
The library for Replit Object Storage
1+
# replit-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: 1083 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[tool.poetry]
2+
name = "replit.storage"
3+
version = "0.0.1.a3"
4+
description = "A library for interacting with Object Storage on Replit"
5+
authors = ["Repl.it <[email protected]>"]
6+
license = "ISC"
7+
readme = "README.md"
8+
repository = "https://github.com/replit/replit-storage-python"
9+
homepage = "https://github.com/replit/replit-storage-python"
10+
classifiers = [
11+
"Programming Language :: Python :: 3",
12+
"License :: OSI Approved :: ISC License (ISCL)",
13+
"Operating System :: OS Independent",
14+
]
15+
packages = [
16+
{ include = "replit", from = "src" },
17+
{ include = "tests" }
18+
]
19+
exclude = ["tests"]
20+
21+
[tool.poetry.dependencies]
22+
python = ">=3.10.0,<3.11"
23+
google-cloud-storage = "^2.14.0"
24+
requests = "^2.31.0"
25+
26+
[tool.poetry.group.dev.dependencies]
27+
ruff = "^0.1.15"
28+
pytest = "^8.0.0"
29+
twine = "*"
30+
pytest-cov = "^4.1.0"
31+
32+
[tool.pyright]
33+
# https://github.com/microsoft/pyright/blob/main/docs/configuration.md
34+
useLibraryCodeForTypes = true
35+
exclude = [".cache"]
36+
37+
[tool.ruff]
38+
# https://beta.ruff.rs/docs/configuration/
39+
indent-width = 4
40+
line-length = 88
41+
select = ['E', 'W', 'F', 'I', 'B', 'D', 'C4', 'ARG', 'SIM']
42+
ignore = ['W291', 'W292', 'W293']
43+
44+
[tool.ruff.per-file-ignores]
45+
"__init__.py" = ["F401"]
46+
"tests/**/*" = ["D"]
47+
48+
[tool.ruff.lint.pydocstyle]
49+
convention = "google"
50+
51+
[build-system]
52+
requires = ["poetry-core>=1.0.0"]
53+
build-backend = "poetry.core.masonry.api"

replit.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{ pkgs }: {
2+
deps = [];
3+
}

src/replit/storage/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Public interface for the replit.storage library."""
2+
3+
from replit.storage.client import Client
4+
from replit.storage.errors import DefaultBucketError

src/replit/storage/client.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Client for interacting with Object Storage. This is the top-level interface.
2+
3+
Note: this Client is a thin wrapper over the GCS Python Library. As a result,
4+
many docstrings are borrowed from the underlying library.
5+
"""
6+
7+
from typing import Optional
8+
9+
import requests
10+
from google.auth import identity_pool
11+
from google.cloud import storage
12+
from replit.storage.config import REPLIT_ADC, REPLIT_DEFAULT_BUCKET_URL
13+
from replit.storage.errors import DefaultBucketError
14+
15+
16+
class Client:
17+
"""Client manages interactions with Replit Object Storage.
18+
19+
If multiple buckets are used within an application, one Client should be used
20+
per bucket.
21+
"""
22+
23+
__gcs_client: storage.Client
24+
25+
__bucket_id: Optional[str] = None
26+
__gcs_bucket_handle: Optional[storage.Bucket] = None
27+
28+
def __init__(self, bucket_id: Optional[str] = None):
29+
"""Creates a new Client.
30+
31+
Args:
32+
bucket_id: The ID of the bucket this Client should interface with.
33+
If no ID is defined, the Repl / Deployment's default bucket will be
34+
used.
35+
"""
36+
creds = identity_pool.Credentials(**REPLIT_ADC)
37+
if bucket_id:
38+
self.__bucket_id = bucket_id
39+
self.__gcs_client = storage.Client(credentials=creds, project="")
40+
self.__gcs_bucket_handle = None
41+
42+
def delete(self, object_name: str) -> None:
43+
"""Deletes an object from Object Storage.
44+
45+
Args:
46+
object_name: The name of the object to be deleted.
47+
"""
48+
return self.__object(object_name).delete()
49+
50+
def download_as_bytes(self, object_name: str) -> bytes:
51+
"""Download the contents an object as a bytes object.
52+
53+
Args:
54+
object_name: The name of the object to be downloaded.
55+
"""
56+
return self.__object(object_name).download_as_bytes()
57+
58+
def download_as_string(self, object_name: str) -> str:
59+
"""Download the contents an object as a string.
60+
61+
Args:
62+
object_name: The name of the object to be downloaded.
63+
"""
64+
return self.__object(object_name).download_as_text()
65+
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+
75+
def download_to_filename(self, object_name: str, dest_filename: str) -> None:
76+
"""Download the contents an object into a file on the local disk.
77+
78+
Args:
79+
object_name: The name of the object to be downloaded.
80+
dest_filename: The filename of the file on the local disk to be written.
81+
"""
82+
return self.__object(object_name).download_to_filename(dest_filename)
83+
84+
def exists(self, object_name: str) -> bool:
85+
"""Checks if an object exist.
86+
87+
Args:
88+
object_name: The name of the object to be checked.
89+
"""
90+
return self.__object(object_name).exists()
91+
92+
def upload_from_file(self, dest_object_name: str, src_file) -> None:
93+
"""Uploads the contents of a file-like object.
94+
95+
Args:
96+
dest_object_name: The name of the object to be uploaded.
97+
src_file: A file-like object.
98+
"""
99+
self.__object(dest_object_name).upload_from_file(src_file)
100+
101+
def upload_from_filename(self, dest_object_name: str,
102+
src_filename: str) -> None:
103+
"""Upload an object from a file on the local disk.
104+
105+
Args:
106+
dest_object_name: The name of the object to be uploaded.
107+
src_filename: The filename of a file on the local disk
108+
"""
109+
self.__object(dest_object_name).upload_from_filename(src_filename)
110+
111+
def upload_from_string(self, dest_object_name: str, src_data: str) -> None:
112+
"""Upload an object from a string.
113+
114+
Args:
115+
dest_object_name: The name of the object to be uploaded.
116+
src_data: The text to be uploaded.
117+
"""
118+
self.__object(dest_object_name).upload_from_string(src_data)
119+
120+
def __object(self, object_name: str) -> storage.Blob:
121+
if self.__gcs_bucket_handle is None:
122+
self.__gcs_bucket_handle = self.__get_bucket_handle()
123+
124+
return self.__gcs_bucket_handle.blob(object_name)
125+
126+
def __get_bucket_handle(self) -> storage.Bucket:
127+
if self.__bucket_id is None:
128+
self.__bucket_id = self.__get_default_bucket_id()
129+
return self.__gcs_client.bucket(self.__bucket_id)
130+
131+
@staticmethod
132+
def __get_default_bucket_id() -> str:
133+
response = requests.get(REPLIT_DEFAULT_BUCKET_URL)
134+
try:
135+
response.raise_for_status()
136+
except requests.HTTPError as exc:
137+
raise DefaultBucketError("failed to request default bucket") from exc
138+
139+
bucket_id = response.json().get("bucketId", "")
140+
if bucket_id == "":
141+
raise DefaultBucketError("no default bucket was specified, it may need "
142+
"to be configured in .replit")
143+
144+
return bucket_id

0 commit comments

Comments
 (0)