Author SHA1 Message Date
jakob.scheid 8233448a63 chore: set up Python project 2026-08-01 19:09:12 +02:00
3 changed files with 2 additions and 131 deletions
+2 -5
View File
@@ -5,14 +5,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "jcloud-s3fuse"
version = "0.1.0"
description = "A userspace filesystem for S3 compatible object stores"
description = "A userspace filesystem for S3 compatible object stores "
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.28.1",
"lxml>=6.0.2"
]
dependencies = []
[project.optional-dependencies]
dev = []
-70
View File
@@ -1,70 +0,0 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import httpx
from .exceptions import S3InternalServerError, S3ObjectNotFoundError, S3ObjectPermissionError
from typing import AsyncGenerator, Optional
from urllib.parse import urljoin
__all__ = [
'S3Backend'
]
class S3Backend:
'''
A class representing an S3 compatible backend.
:param url: The URL of the backend.
:type url: str
'''
def __init__(self, url: str) -> None:
self.url = url
async def get_object(self, object_path: str, range: Optional[str] = None) -> AsyncGenerator[bytes]:
'''
Returns the bytes of an object.
:param object_path: The path of the object. It consists of the
bucket name and the object key.
:type object_path: str
:param range: The bytes range as an RFC 9110-compliant string.
:type range: Optional[str]
:return: An iterator yielding the object bytes.
:rtype: AsyncGenerator[bytes]
'''
if range is not None:
headers = {
'Range': f'bytes={range}'
}
else:
headers = {}
async with httpx.AsyncClient() as client:
async with client.stream(
'GET',
urljoin(self.url, object_path),
headers = headers
) as response:
if response.status_code == 404:
raise S3ObjectNotFoundError('Object not found', object_path = object_path)
if response.status_code == 403:
raise S3ObjectPermissionError('Object access forbidden', object_path = object_path)
if response.status_code // 100 == 5:
raise S3InternalServerError('Internal server error')
async for chunk in response.aiter_bytes():
yield chunk
-56
View File
@@ -1,56 +0,0 @@
# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = [
'S3Error',
'S3InternalServerError',
'S3ObjectError',
'S3ObjectNotFoundError',
'S3ObjectPermissionError'
]
class S3Error(Exception):
'''
Base class for all S3 related errors.
'''
class S3InternalServerError(S3Error):
'''
Internal S3 server error.
'''
class S3ObjectError(S3Error):
'''
Base class for S3 object related errors.
'''
def __init__(self, *args: object, object_path: str = '') -> None:
super().__init__(*args)
self.object_path = object_path
def __str__(self):
if self.args:
return f'{self.args[0]}{": " if self.object_path and self.args[0] else ""}{self.object_path if self.args[0] else ""}'
else:
return ''
class S3ObjectNotFoundError(S3ObjectError):
'''
S3 object was not found.
'''
class S3ObjectPermissionError(S3ObjectError):
'''
Bad permissions to retrieve S3 object.
'''