# 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