From 4167d0ae7c51e1e885bf4997b9c0be851d8fd553 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Thu, 11 Apr 2024 15:10:53 -0700 Subject: [PATCH] liblab SDK update for version v0.0.4 --- .devcontainer/devcontainer.json | 12 +- .env.example | 2 +- .gitignore | 144 +--- .manifest.json | 163 ++++ LICENSE | 18 +- README.md | 693 +++++++++--------- examples/.env.example | 1 + examples/install.cmd | 6 + examples/install.sh | 7 +- examples/sample.py | 11 +- install.cmd | 22 + install.sh | 29 +- pyproject.toml | 10 +- src/llama_store/__init__.py | 2 + src/llama_store/hooks/__init__.py | 0 src/llama_store/hooks/hook.py | 37 + src/llama_store/models/__init__.py | 8 + src/llama_store/models/api_token.py | 17 + src/llama_store/models/api_token_request.py | 17 + src/llama_store/models/base.py | 272 +++++++ src/llama_store/models/llama.py | 30 + src/llama_store/models/llama_color.py | 28 + src/llama_store/models/llama_create.py | 25 + src/llama_store/models/llama_id.py | 14 + src/llama_store/models/user.py | 17 + src/llama_store/models/user_registration.py | 17 + src/llama_store/models/utils/cast_models.py | 81 ++ src/llama_store/models/utils/json_map.py | 80 ++ src/llama_store/net/__init__.py | 0 src/llama_store/net/environment/__init__.py | 1 + .../net/environment/environment.py | 11 + src/llama_store/net/headers/__init__.py | 0 .../net/headers/access_token_auth.py | 41 ++ src/llama_store/net/headers/base_header.py | 9 + src/llama_store/net/request_chain/__init__.py | 0 .../net/request_chain/handlers/__init__.py | 0 .../request_chain/handlers/base_handler.py | 39 + .../request_chain/handlers/hook_handler.py | 47 ++ .../request_chain/handlers/http_handler.py | 80 ++ .../request_chain/handlers/retry_handler.py | 65 ++ .../net/request_chain/request_chain.py | 57 ++ src/llama_store/net/transport/__init__.py | 0 src/llama_store/net/transport/request.py | 89 +++ .../net/transport/request_error.py | 53 ++ src/llama_store/net/transport/response.py | 59 ++ src/llama_store/net/transport/serializer.py | 249 +++++++ src/llama_store/net/transport/utils.py | 28 + src/llama_store/sdk.py | 44 ++ src/llama_store/services/__init__.py | 0 src/llama_store/services/llama.py | 145 ++++ src/llama_store/services/llama_picture.py | 138 ++++ src/llama_store/services/token.py | 37 + src/llama_store/services/user.py | 67 ++ .../services/utils/base_service.py | 74 ++ .../services/utils/default_headers.py | 57 ++ src/llama_store/services/utils/validator.py | 216 ++++++ 56 files changed, 2852 insertions(+), 517 deletions(-) create mode 100644 .manifest.json create mode 100644 examples/.env.example create mode 100644 examples/install.cmd create mode 100644 install.cmd create mode 100644 src/llama_store/__init__.py create mode 100644 src/llama_store/hooks/__init__.py create mode 100644 src/llama_store/hooks/hook.py create mode 100644 src/llama_store/models/__init__.py create mode 100644 src/llama_store/models/api_token.py create mode 100644 src/llama_store/models/api_token_request.py create mode 100644 src/llama_store/models/base.py create mode 100644 src/llama_store/models/llama.py create mode 100644 src/llama_store/models/llama_color.py create mode 100644 src/llama_store/models/llama_create.py create mode 100644 src/llama_store/models/llama_id.py create mode 100644 src/llama_store/models/user.py create mode 100644 src/llama_store/models/user_registration.py create mode 100644 src/llama_store/models/utils/cast_models.py create mode 100644 src/llama_store/models/utils/json_map.py create mode 100644 src/llama_store/net/__init__.py create mode 100644 src/llama_store/net/environment/__init__.py create mode 100644 src/llama_store/net/environment/environment.py create mode 100644 src/llama_store/net/headers/__init__.py create mode 100644 src/llama_store/net/headers/access_token_auth.py create mode 100644 src/llama_store/net/headers/base_header.py create mode 100644 src/llama_store/net/request_chain/__init__.py create mode 100644 src/llama_store/net/request_chain/handlers/__init__.py create mode 100644 src/llama_store/net/request_chain/handlers/base_handler.py create mode 100644 src/llama_store/net/request_chain/handlers/hook_handler.py create mode 100644 src/llama_store/net/request_chain/handlers/http_handler.py create mode 100644 src/llama_store/net/request_chain/handlers/retry_handler.py create mode 100644 src/llama_store/net/request_chain/request_chain.py create mode 100644 src/llama_store/net/transport/__init__.py create mode 100644 src/llama_store/net/transport/request.py create mode 100644 src/llama_store/net/transport/request_error.py create mode 100644 src/llama_store/net/transport/response.py create mode 100644 src/llama_store/net/transport/serializer.py create mode 100644 src/llama_store/net/transport/utils.py create mode 100644 src/llama_store/sdk.py create mode 100644 src/llama_store/services/__init__.py create mode 100644 src/llama_store/services/llama.py create mode 100644 src/llama_store/services/llama_picture.py create mode 100644 src/llama_store/services/token.py create mode 100644 src/llama_store/services/user.py create mode 100644 src/llama_store/services/utils/base_service.py create mode 100644 src/llama_store/services/utils/default_headers.py create mode 100644 src/llama_store/services/utils/validator.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 428b4b4..25e063a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,12 +1,10 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/python { - "name": "Python SDK", - "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", - "postCreateCommand": "pip3 install --user -r requirements.txt && cd examples && sh install.sh", + "name": "LlamaStore", + "image": "mcr.microsoft.com/devcontainers/python:3.9-bullseye", + "postCreateCommand": "bash install.sh", "customizations": { "codespaces":{ - "openFiles": ["examples/sample.py", "examples/README.md"] + "openFiles": ["examples/sample.py"] } - } + } } diff --git a/.env.example b/.env.example index ebe4cdc..8b13789 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -LLAMASTORE_BEARER_TOKEN= + diff --git a/.gitignore b/.gitignore index 68bc17f..cebf901 100644 --- a/.gitignore +++ b/.gitignore @@ -3,122 +3,10 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - # Installer logs pip-log.txt pip-delete-this-directory.txt -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -128,33 +16,5 @@ ENV/ env.bak/ venv.bak/ -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# Devcontainer +.devcontainer/ diff --git a/.manifest.json b/.manifest.json new file mode 100644 index 0000000..0ff9e40 --- /dev/null +++ b/.manifest.json @@ -0,0 +1,163 @@ +{ + "liblabVersion": "2.1.2", + "date": "2024-04-11T22:08:46.245Z", + "config": { + "apiId": 543, + "baseUrl": "http://localhost:8080", + "sdkName": "llama-store", + "sdkVersion": "0.0.4", + "liblabVersion": "2", + "deliveryMethods": ["zip"], + "languages": ["python"], + "specFilePath": "spec.json", + "createDocs": true, + "auth": ["bearer"], + "languageOptions": { + "csharp": { + "githubRepoName": "llama-store-sdk-csharp", + "sdkVersion": "0.0.4" + }, + "go": { + "goModuleName": "github.com/liblaber/llama-store-sdk-go", + "githubRepoName": "llama-store-sdk-go", + "sdkVersion": "0.0.4" + }, + "java": { + "groupId": "com.liblab", + "githubRepoName": "llama-store-sdk-java", + "sdkVersion": "0.0.4" + }, + "python": { + "alwaysInitializeOptionals": true, + "pypiPackageName": "LlamaStore", + "githubRepoName": "llama-store-sdk-python", + "liblabVersion": "2", + "sdkVersion": "0.0.4" + }, + "typescript": { + "bundle": true, + "exportClassDefault": true, + "httpClient": "axios", + "githubRepoName": "llama-store-sdk-typescript", + "sdkVersion": "0.0.4" + } + }, + "publishing": { + "githubOrg": "liblaber" + }, + "devContainer": true, + "generateEnv": true, + "includeOptionalSnippetParameters": true, + "inferServiceNames": false, + "license": { + "type": "MIT", + "name": "MIT", + "url": "https://opensource.org/licenses/MIT", + "path": "MIT.ejs" + }, + "responseHeaders": false, + "authentication": { + "access": { + "prefix": "Bearer" + } + }, + "multiTenant": true, + "hooksLocation": { + "bucketKey": "4850/hooks.zip", + "bucketName": "prod-liblab-api-stack-hooks" + }, + "specUrl": "https://prod-liblab-api-stack-specs.s3.us-east-1.amazonaws.com/543/open-api-spec.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIA5P3QKKDKGVNIJ2H7%2F20240411%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240411T220842Z&X-Amz-Expires=43200&X-Amz-Signature=29a99a4a3934b84c7b33cfd307c2a3de675ce09b2c61a7430b307070dcc72190&X-Amz-SignedHeaders=host&x-id=GetObject", + "includeWatermark": false, + "alwaysInitializeOptionals": true, + "pypiPackageName": "LlamaStore", + "githubRepoName": "llama-store-sdk-python", + "language": "python", + "deliveryMethod": "zip", + "hooks": { + "enabled": true, + "sourceDir": "/tmp/resources/hooks" + }, + "usesFormData": false, + "environmentVariables": [], + "fileOutput": "/tmp", + "httpLibrary": { + "name": "axios", + "packages": { + "axios": "^1.6.8" + }, + "languages": ["typescript"] + }, + "retry": { + "enabled": true, + "maxAttempts": 3, + "retryDelay": 150, + "maxDelay": 5000, + "retryDelayJitter": 50, + "backOffFactor": 2, + "httpCodesToRetry": [408, 429, 500, 502, 503, 504], + "httpMethodsToRetry": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] + }, + "customQueries": { + "paths": [], + "rawQueries": [], + "queriesData": [] + } + }, + "files": [ + "src/llama_store/models/base.py", + "src/llama_store/models/utils/json_map.py", + "src/llama_store/models/__init__.py", + "src/llama_store/services/__init__.py", + "src/llama_store/__init__.py", + "src/llama_store/net/__init__.py", + "src/llama_store/net/headers/__init__.py", + "src/llama_store/net/transport/__init__.py", + "src/llama_store/hooks/__init__.py", + "src/llama_store/net/environment/__init__.py", + "src/llama_store/net/request_chain/__init__.py", + "src/llama_store/net/request_chain/handlers/__init__.py", + "src/llama_store/net/transport/request.py", + "src/llama_store/net/transport/response.py", + "src/llama_store/net/transport/request_error.py", + "src/llama_store/sdk.py", + "src/llama_store/net/transport/serializer.py", + "src/llama_store/services/utils/validator.py", + "src/llama_store/services/utils/base_service.py", + "pyproject.toml", + ".gitignore", + "src/llama_store/models/utils/cast_models.py", + "src/llama_store/net/transport/utils.py", + "src/llama_store/hooks/hook.py", + "install.sh", + "install.cmd", + "examples/install.sh", + "examples/install.cmd", + "src/llama_store/net/headers/base_header.py", + "src/llama_store/net/headers/access_token_auth.py", + "src/llama_store/net/environment/environment.py", + "src/llama_store/net/request_chain/request_chain.py", + "src/llama_store/net/request_chain/handlers/base_handler.py", + "src/llama_store/net/request_chain/handlers/http_handler.py", + "src/llama_store/net/request_chain/handlers/hook_handler.py", + "src/llama_store/net/request_chain/handlers/retry_handler.py", + "examples/sample.py", + ".env.example", + "/examples/.env.example", + "src/llama_store/services/utils/default_headers.py", + "./LICENSE", + ".devcontainer/devcontainer.json", + "src/llama_store/services/token.py", + "src/llama_store/models/llama_id.py", + "src/llama_store/models/llama.py", + "src/llama_store/models/llama_create.py", + "src/llama_store/models/api_token_request.py", + "src/llama_store/models/api_token.py", + "src/llama_store/models/user.py", + "src/llama_store/models/user_registration.py", + "src/llama_store/models/llama_color.py", + "src/llama_store/services/user.py", + "src/llama_store/services/llama_picture.py", + "src/llama_store/services/llama.py", + "README.md" + ] +} diff --git a/LICENSE b/LICENSE index cb511c8..db5ca62 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (c) 2023 +MIT License + +Copyright (c) 2024 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -10,10 +12,10 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9c71c26..73a6f85 100644 --- a/README.md +++ b/README.md @@ -1,520 +1,535 @@ -# Llamastore Python SDK 0.0.1 -A Python SDK for Llamastore. +# LlamaStore Python SDK 0.0.4 -The llama store API! Get details on all your favorite llamas. ## To use this API - You will need to register a user, once done you can request an API token. - You can then use your API token to get details about the llamas. ## User registration To register a user, send a POST request to `/user` with the following body: ```json { email : , password : } ``` This API has a maximum of 1000 current users. Once this is exceeded, older users will be deleted. If your user is deleted, you will need to register again. ## Get an API token To get an API token, send a POST request to `/token` with the following body: ```json { email : , password : } ``` This will return a token that you can use to authenticate with the API: ```json { access_token : , token_type : bearer } ``` ## Use the API token To use the API token, add it to the `Authorization` header of your request: ``` Authorization: Bearer ``` +A Python SDK for LlamaStore. -- API version: 0.0.1 -- SDK version: 0.0.1 +- API version: 0.1.7 +- SDK version: 0.0.4 + +The llama store API! Get details on all your favorite llamas. ## To use this API - You will need to register a user, once done you can request an API token. - You can then use your API token to get details about the llamas. ## User registration To register a user, send a POST request to `/user` with the following body: `json { email : , password : } ` This API has a maximum of 1000 current users. Once this is exceeded, older users will be deleted. If your user is deleted, you will need to register again. ## Get an API token To get an API token, send a POST request to `/token` with the following body: `json { email : , password : } ` This will return a token that you can use to authenticate with the API: `json { access_token : , token_type : bearer } ` ## Use the API token To use the API token, add it to the `Authorization` header of your request: `Authorization: Bearer ` ## Table of Contents -- [Requirements](#requirements) + - [Installation](#installation) - - [Dependencies](#dependencies) - [Authentication](#authentication) - - [Access Token Authentication](#bearer-authentication) -- [API Endpoint Services](#api-endpoint-services) -- [API Models](#api-models) -- [Testing](#testing) -- [Configuration](#configuration) -- [Sample Usage](#sample-usage) -- [Llamastore Services](#llamastore-services) -- [License](#license) +- [Services](#services) ## Installation + ```bash -pip install llamastore +pip install LlamaStore ``` -### Dependencies - -This SDK uses the following dependencies: -- requests 2.28.1 -- http-exceptions 0.2.10 -- pytest 7.1.2 -- responses 0.21.0 - ## Authentication -To see whether an endpoint needs a specific type of authentication check the endpoint's documentation. +### Access Token -### Access Token Authentication -The Llamastore API uses bearer tokens as a form of authentication.You can set the bearer token when initializing the SDK through the constructor: +The LlamaStore API uses a access token as a form of authentication. -```py -sdk = Llamastore('YOUR_BEARER_TOKEN') -``` +The access token can be set when initializing the SDK like this: -Or through the `set_access_token` method: ```py -sdk = Llamastore() -sdk.set_access_token('YOUR_BEARER_TOKEN') +LlamaStore( + access_token="YOUR_ACCESS_TOKEN" +) ``` -You can also set it for each service individually: +Or at a later stage: + ```py -sdk = Llamastore() -sdk.llama.set_access_token('YOUR_BEARER_TOKEN') +sdk.set_access_token("YOUR_ACCESS_TOKEN") ``` -## API Endpoint Services +## Services -All URIs are relative to http://localhost:8000. +A list of all SDK services. Click on the service name to access its corresponding service methods. -Click the service name for a full list of the service methods. +| Service | +| :------------------------------------------ | +| [LlamaPictureService](#llamapictureservice) | +| [LlamaService](#llamaservice) | +| [TokenService](#tokenservice) | +| [UserService](#userservice) | -| Service | -| :------ | -|[LlamaPicture](./services/README.md#llamapicture)| -|[Llama](./services/README.md#llama)| -|[Token](./services/README.md#token)| -|[User](./services/README.md#user)| +### LlamaPictureService -## API Models -[A list documenting all API models for this SDK](./models/README.md#llamastore-models). +A list of all methods in the `LlamaPictureService` service. Click on the method name to view detailed information about that method. -## Testing +| Methods | Description | +| :-------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | +| [get_llama_picture_by_llama_id](#get_llama_picture_by_llama_id) | Get a llama's picture by the llama ID. Pictures are in PNG format. | +| [create_llama_picture](#create_llama_picture) | Create a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. | +| [update_llama_picture](#update_llama_picture) | Update a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. | -Run unit tests with this command: +If the llama does not have a picture, one will be created. If the llama already has a picture, +it will be overwritten. +If the llama does not exist, a 404 will be returned. | +|[delete_llama_picture](#delete_llama_picture)| Delete a llama's picture by ID. | -```sh -python -m unittest discover -p "test*.py" -``` +#### **get_llama_picture_by_llama_id** -## Sample Usage +Get a llama's picture by the llama ID. Pictures are in PNG format. -```py -from os import getenv -from pprint import pprint -from llamastore import Llamastore - -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) - -results = sdk.llama.get_llamas() - -pprint(vars(results)) -``` +- HTTP Method: `GET` +- Endpoint: `/llama/{llama_id}/picture` +**Parameters** +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| llama_id | int | ✅ | Get a llama's picture by the llama ID. Pictures are in PNG format. | -# Llamastore Services -A list of all services and services methods. -- Services +**Return Type** - - [LlamaPicture](#llamapicture) +`bytes` - - [Llama](#llama) +**Example Usage Code Snippet** - - [Token](#token) +```py +from llama_store import LlamaStore, Environment - - [User](#user) -- [All Methods](#all-methods) +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) +result = sdk.llama_picture.get_llama_picture_by_llama_id(llama_id="1") -## LlamaPicture +with open("output-image.png", "wb") as f: + f.write(result) +``` -| Method | Description| -| :-------- | :----------| -| [create_llama_picture](#create_llama_picture) | Create Llama Picture | -| [get_llama_picture_by_llama_id](#get_llama_picture_by_llama_id) | Get Llama Picture | -| [delete_llama_picture](#delete_llama_picture) | Delete Llama Picture | -| [update_llama_picture](#update_llama_picture) | Update Llama Picture | +#### **create_llama_picture** +Create a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. -## Llama +- HTTP Method: `POST` +- Endpoint: `/llama/{llama_id}/picture` -| Method | Description| -| :-------- | :----------| -| [create_llama](#create_llama) | Create Llama | -| [get_llamas](#get_llamas) | Get Llamas | -| [get_llama_by_id](#get_llama_by_id) | Get Llama | -| [delete_llama](#delete_llama) | Delete Llama | -| [update_llama](#update_llama) | Update Llama | +**Parameters** +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | bytes | ❌ | The request body. | +| llama_id | int | ✅ | Create a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. | +**Return Type** -## Token +`LlamaId` -| Method | Description| -| :-------- | :----------| -| [create_api_token](#create_api_token) | Create Api Token | +**Example Usage Code Snippet** +```py +from llama_store import LlamaStore, Environment +from llama_store.models import any -## User +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) -| Method | Description| -| :-------- | :----------| -| [get_user_by_email](#get_user_by_email) | Get User By Email | -| [register_user](#register_user) | Register User | +with open("image.png", "rb") as f: + request_body = f.read() +result = sdk.llama_picture.create_llama_picture( + request_body=request_body, + llama_id="1" +) +print(result) +``` +#### **update_llama_picture** -## All Methods +Update a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. +If the llama does not have a picture, one will be created. If the llama already has a picture, +it will be overwritten. +If the llama does not exist, a 404 will be returned. -### **create_llama_picture** -Create Llama Picture -- HTTP Method: POST -- Endpoint: /llama/{llama_id}/picture +- HTTP Method: `PUT` +- Endpoint: `/llama/{llama_id}/picture` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The ID of the llama that this picture is for | -| request_input | object | Optional | Request body. | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | bytes | ❌ | The request body. | +| llama_id | int | ✅ | Update a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. + +If the llama does not have a picture, one will be created. If the llama already has a picture, +it will be overwritten. +If the llama does not exist, a 404 will be returned. | **Return Type** -[LlamaId](/src/llamastore/models/README.md#llamaid) +`LlamaId` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = {} -results = sdk.llama_picture.create_llama_picture( - request_input = request_body, - llama_id = 1 + +```py +from llama_store import LlamaStore, Environment +from llama_store.models import any + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value ) -pprint(vars(results)) +with open("image.png", "rb") as f: + request_body = f.read() + +result = sdk.llama_picture.update_llama_picture( + request_body=request_body, + llama_id="1" +) +print(result) ``` -### **get_llama_picture_by_llama_id** -Get Llama Picture -- HTTP Method: GET -- Endpoint: /llama/{llama_id}/picture +#### **delete_llama_picture** -**Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The ID of the llama to get the picture for | +Delete a llama's picture by ID. -**Return Type** +- HTTP Method: `DELETE` +- Endpoint: `/llama/{llama_id}/picture` -Returns a dict object. +**Parameters** +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| llama_id | int | ✅ | Delete a llama's picture by ID. | **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.llama_picture.get_llama_picture_by_llama_id(llama_id = 2) -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) + +result = sdk.llama_picture.delete_llama_picture(llama_id="1") +print(result) ``` -### **delete_llama_picture** -Delete Llama Picture -- HTTP Method: DELETE -- Endpoint: /llama/{llama_id}/picture +### LlamaService + +A list of all methods in the `LlamaService` service. Click on the method name to view detailed information about that method. + +| Methods | Description | +| :---------------------------------- | :------------------------------------------------------ | +| [get_llamas](#get_llamas) | Get all the llamas. | +| [create_llama](#create_llama) | Create a new llama. Llama names must be unique. | +| [get_llama_by_id](#get_llama_by_id) | Get a llama by ID. | +| [update_llama](#update_llama) | Update a llama. If the llama does not exist, create it. | + +When updating a llama, the llama name must be unique. If the llama name is not unique, a 409 will be returned. | +|[delete_llama](#delete_llama)| Delete a llama. If the llama does not exist, this will return a 404. | + +#### **get_llamas** + +Get all the llamas. + +- HTTP Method: `GET` +- Endpoint: `/llama` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The ID of the llama to delete the picture for | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| **Return Type** -Returns a dict object. +`List[Llama]` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.llama_picture.delete_llama_picture(llama_id = 2) -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) +result = sdk.llama.get_llamas() + +print(result) ``` -### **update_llama_picture** -Update Llama Picture -- HTTP Method: PUT -- Endpoint: /llama/{llama_id}/picture +#### **create_llama** + +Create a new llama. Llama names must be unique. + +- HTTP Method: `POST` +- Endpoint: `/llama` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The ID of the llama that this picture is for | -| request_input | object | Optional | Request body. | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | LlamaCreate | ✅ | The request body. | **Return Type** -[LlamaId](/src/llamastore/models/README.md#llamaid) +`Llama` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = {} -results = sdk.llama_picture.update_llama_picture( - request_input = request_body, - llama_id = 1 + +```py +from llama_store import LlamaStore, Environment +from llama_store.models import LlamaCreate + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value ) -pprint(vars(results)) +request_body = LlamaCreate(**{ + "name": "libby the llama", + "age": 5, + "color": "brown", + "rating": 1 +}) + +result = sdk.llama.create_llama(request_body=request_body) +print(result) ``` +#### **get_llama_by_id** -### **create_llama** -Create Llama -- HTTP Method: POST -- Endpoint: /llama +Get a llama by ID. + +- HTTP Method: `GET` +- Endpoint: `/llama/{llama_id}` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| request_input | [LlamaCreate](/src/llamastore/models/README.md#llamacreate) | Required | Request body. | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| llama_id | int | ✅ | Get a llama by ID. | **Return Type** -[Llama](/src/llamastore/models/README.md#llama) +`Llama` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = { - 'age': 5, - 'color': 'brown', - 'name': 'libby the llama', - 'rating': 4 -} -results = sdk.llama.create_llama(request_input = request_body) - -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) + +result = sdk.llama.get_llama_by_id(llama_id="1") + +print(result) ``` -### **get_llamas** -Get Llamas -- HTTP Method: GET -- Endpoint: /llama +#### **update_llama** + +Update a llama. If the llama does not exist, create it. + +When updating a llama, the llama name must be unique. If the llama name is not unique, a 409 will be returned. + +- HTTP Method: `PUT` +- Endpoint: `/llama/{llama_id}` **Parameters** +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | LlamaCreate | ✅ | The request body. | +| llama_id | int | ✅ | Update a llama. If the llama does not exist, create it. -This method has no parameters. +When updating a llama, the llama name must be unique. If the llama name is not unique, a 409 will be returned. | **Return Type** -[GetLlamasResponse](/src/llamastore/models/README.md#getllamasresponse) +`Llama` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.llama.get_llamas() -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment +from llama_store.models import LlamaCreate + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) + +request_body = LlamaCreate(**{ + "name": "libby the llama", + "age": 5, + "color": "brown", + "rating": 1 +}) +result = sdk.llama.update_llama( + request_body=request_body, + llama_id="1" +) + +print(result) ``` -### **get_llama_by_id** -Get Llama -- HTTP Method: GET -- Endpoint: /llama/{llama_id} +#### **delete_llama** -**Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The llama's ID | +Delete a llama. If the llama does not exist, this will return a 404. -**Return Type** +- HTTP Method: `DELETE` +- Endpoint: `/llama/{llama_id}` -[Llama](/src/llamastore/models/README.md#llama) +**Parameters** +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| llama_id | int | ✅ | Delete a llama. If the llama does not exist, this will return a 404. | **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.llama.get_llama_by_id(llama_id = 1) -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) + +result = sdk.llama.delete_llama(llama_id="1") +print(result) ``` -### **delete_llama** -Delete Llama -- HTTP Method: DELETE -- Endpoint: /llama/{llama_id} +### TokenService -**Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The llama's ID | +A list of all methods in the `TokenService` service. Click on the method name to view detailed information about that method. -**Return Type** +| Methods | Description | +| :------------------------------------ | :-------------------------------------------------------------------- | +| [create_api_token](#create_api_token) | Create an API token for a user. These tokens expire after 30 minutes. | -Returns a dict object. +Once you have this token, you need to pass it to other endpoints in the Authorization header as a Bearer token. | -**Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.llama.delete_llama(llama_id = 1) +#### **create_api_token** -pprint(vars(results)) +Create an API token for a user. These tokens expire after 30 minutes. -``` +Once you have this token, you need to pass it to other endpoints in the Authorization header as a Bearer token. -### **update_llama** -Update Llama -- HTTP Method: PUT -- Endpoint: /llama/{llama_id} +- HTTP Method: `POST` +- Endpoint: `/token` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| llama_id | int | Required | The llama's ID | -| request_input | [LlamaCreate](/src/llamastore/models/README.md#llamacreate) | Required | Request body. | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | ApiTokenRequest | ✅ | The request body. | **Return Type** -[Llama](/src/llamastore/models/README.md#llama) +`ApiToken` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = { - 'age': 5, - 'color': 'brown', - 'name': 'libby the llama', - 'rating': 4 -} -results = sdk.llama.update_llama( - request_input = request_body, - llama_id = 2 + +```py +from llama_store import LlamaStore, Environment +from llama_store.models import ApiTokenRequest + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value ) -pprint(vars(results)) +request_body = ApiTokenRequest(**{ + "email": "7pe6?Z`06@+@mIcksLu.^Yk|\\@rd'#", + "password": "Password123!" +}) +result = sdk.token.create_api_token(request_body=request_body) + +print(result) ``` +### UserService -### **create_api_token** -Create Api Token -- HTTP Method: POST -- Endpoint: /token +A list of all methods in the `UserService` service. Click on the method name to view detailed information about that method. -**Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| request_input | [ApiTokenRequest](/src/llamastore/models/README.md#apitokenrequest) | Required | Request body. | +| Methods | Description | +| :-------------------------------------- | :------------------- | +| [get_user_by_email](#get_user_by_email) | Get a user by email. | -**Return Type** +This endpoint will return a 404 if the user does not exist. Otherwise, it will return a 200. | +|[register_user](#register_user)| Register a new user. -[ApiToken](/src/llamastore/models/README.md#apitoken) +This endpoint will return a 400 if the user already exists. Otherwise, it will return a 201. | -**Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = { - 'email': 'noone@example.com', - 'password': 'Password123!' -} -results = sdk.token.create_api_token(request_input = request_body) - -pprint(vars(results)) +#### **get_user_by_email** -``` +Get a user by email. +This endpoint will return a 404 if the user does not exist. Otherwise, it will return a 200. -### **get_user_by_email** -Get User By Email -- HTTP Method: GET -- Endpoint: /user/{email} +- HTTP Method: `GET` +- Endpoint: `/user/{email}` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| email | str | Required | The user's email address | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| email | str | ✅ | Get a user by email. + +This endpoint will return a 404 if the user does not exist. Otherwise, it will return a 200. | **Return Type** -[User](/src/llamastore/models/README.md#user) +`User` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -results = sdk.user.get_user_by_email(email = 'tIAimCb@N.sUaR].pV') -pprint(vars(results)) +```py +from llama_store import LlamaStore, Environment + +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) + +result = sdk.user.get_user_by_email(email="R6*VwL:,QI@5Y8.G6") +print(result) ``` -### **register_user** -Register User -- HTTP Method: POST -- Endpoint: /user +#### **register_user** + +Register a new user. + +This endpoint will return a 400 if the user already exists. Otherwise, it will return a 201. + +- HTTP Method: `POST` +- Endpoint: `/user` **Parameters** -| Name | Type| Required | Description | -| :-------- | :----------| :----------| :----------| -| request_input | [UserRegistration](/src/llamastore/models/README.md#userregistration) | Required | Request body. | +| Name | Type| Required | Description | +| :-------- | :----------| :----------:| :----------| +| request_body | UserRegistration | ✅ | The request body. | **Return Type** -[User](/src/llamastore/models/README.md#user) +`User` **Example Usage Code Snippet** -```Python -from os import getenv -from pprint import pprint -from llamastore import Llamastore -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) -request_body = { - 'email': 'noone@example.com', - 'password': 'Password123!' -} -results = sdk.user.register_user(request_input = request_body) - -pprint(vars(results)) - -``` - +```py +from llama_store import LlamaStore, Environment +from llama_store.models import UserRegistration +sdk = LlamaStore( + access_token="YOUR_ACCESS_TOKEN", + base_url=Environment.DEFAULT.value +) +request_body = UserRegistration(**{ + "email": "DU\"w@+H,S+E@|g4%6>?Ax?.ETKmcVGon", + "password": "Password123!" +}) -## License +result = sdk.user.register_user(request_body=request_body) -License: MIT. See license in LICENSE. +print(result) +``` diff --git a/examples/.env.example b/examples/.env.example new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/.env.example @@ -0,0 +1 @@ + diff --git a/examples/install.cmd b/examples/install.cmd new file mode 100644 index 0000000..6111197 --- /dev/null +++ b/examples/install.cmd @@ -0,0 +1,6 @@ +python -m venv .venv +call .venv\Scripts\activate +pip install build +python -m build --outdir dist ..\ +pip install dist\LlamaStore-0.0.4-py3-none-any.whl --force-reinstall +deactivate diff --git a/examples/install.sh b/examples/install.sh index e46ed73..832c8a6 100644 --- a/examples/install.sh +++ b/examples/install.sh @@ -1,5 +1,6 @@ python -m venv .venv -source .venv/bin/activate +. .venv/bin/activate pip install build -python -m build --outdir dist ../ -pip install dist/llamastore-0.0.1-py3-none-any.whl +python -m build --outdir dist ../ +pip install dist/LlamaStore-0.0.4-py3-none-any.whl --force-reinstall +deactivate diff --git a/examples/sample.py b/examples/sample.py index 32a70e3..d0dcbc0 100644 --- a/examples/sample.py +++ b/examples/sample.py @@ -1,10 +1,7 @@ -from os import getenv -from pprint import pprint -from llamastore import Llamastore +from llama_store import LlamaStore, Environment -sdk = Llamastore() -sdk.set_access_token(getenv("LLAMASTORE_ACCESS_TOKEN")) +sdk = LlamaStore(access_token="YOUR_ACCESS_TOKEN", base_url=Environment.DEFAULT.value) -results = sdk.llama.get_llamas() +result = sdk.llama.get_llamas() -pprint(vars(results)) +print(result) diff --git a/install.cmd b/install.cmd new file mode 100644 index 0000000..216bbe9 --- /dev/null +++ b/install.cmd @@ -0,0 +1,22 @@ +@echo off +set USE_VENV=0 + +:loop +if "%~1"=="" goto afterloop +if "%~1"=="--use-venv" set USE_VENV=1 +shift +goto loop +:afterloop + +if "%USE_VENV%"=="1" ( + python -m venv .venv + call .venv\Scripts\activate +) + +pip install build +python -m build --outdir dist . +pip install dist\LlamaStore-0.0.4-py3-none-any.whl --force-reinstall + +if "%USE_VENV%"=="1" ( + deactivate +) diff --git a/install.sh b/install.sh index f0a5901..ac13b98 100644 --- a/install.sh +++ b/install.sh @@ -1,3 +1,26 @@ -pip3 install -r requirements.txt -pip3 install -e src/llamastore/ -python3 -m unittest discover -p "test*.py" +#!/bin/bash + +USE_VENV=0 + +for arg in "$@" +do + case $arg in + --use-venv) + USE_VENV=1 + shift + ;; + esac +done + +if [ "$USE_VENV" -eq 1 ]; then + python -m venv .venv + . .venv/bin/activate +fi + +pip install build +python -m build --outdir dist . +pip install dist/LlamaStore-0.0.4-py3-none-any.whl --force-reinstall + +if [ "$USE_VENV" -eq 1 ]; then + deactivate +fi \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 97cee35..df70e2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,16 +3,12 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "llamastore" -version = "0.0.1" +name = "LlamaStore" +version = "0.0.4" license = { text = "MIT" } description = """The llama store API! Get details on all your favorite llamas. ## To use this API - You will need to register a user, once done you can request an API token. - You can then use your API token to get details about the llamas. ## User registration To register a user, send a POST request to `/user` with the following body: ```json { email : , password : } ``` This API has a maximum of 1000 current users. Once this is exceeded, older users will be deleted. If your user is deleted, you will need to register again. ## Get an API token To get an API token, send a POST request to `/token` with the following body: ```json { email : , password : } ``` This will return a token that you can use to authenticate with the API: ```json { access_token : , token_type : bearer } ``` ## Use the API token To use the API token, add it to the `Authorization` header of your request: ``` Authorization: Bearer ``` """ readme = "README.md" requires-python = ">=3.7" dependencies = [ - "requests", - "http-exceptions", - "pytest", - "responses" + "requests>=2.31.0" ] - diff --git a/src/llama_store/__init__.py b/src/llama_store/__init__.py new file mode 100644 index 0000000..6c8fa20 --- /dev/null +++ b/src/llama_store/__init__.py @@ -0,0 +1,2 @@ +from .sdk import LlamaStore +from .net.environment import Environment diff --git a/src/llama_store/hooks/__init__.py b/src/llama_store/hooks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/hooks/hook.py b/src/llama_store/hooks/hook.py new file mode 100644 index 0000000..7b1f654 --- /dev/null +++ b/src/llama_store/hooks/hook.py @@ -0,0 +1,37 @@ +class Request: + def __init__(self, method, url, headers, body=""): + self.method = method + self.url = url + self.headers = headers + self.body = body + + def __str__(self): + return f"method={self.method}, url={self.url}, headers={self.headers}, body={self.body})" + + +class Response: + def __init__(self, status, headers, body): + self.status = status + self.headers = headers + self.body = body + + def __str__(self): + return "Response(status={}, headers={}, body={})".format( + self.status, self.headers, self.body + ) + + +class CustomHook: + + def before_request(self, request: Request): + print( + f"Before request on URL {request.url} with method {request.method.upper()}" + ) + + def after_response(self, request: Request, response: Response): + print( + f"After response on URL {request.url} with method {request.method.upper()}, returning status {response.status}" + ) + + def on_error(self, error: Exception, request: Request, response: Response): + print(f"On error - {error}") diff --git a/src/llama_store/models/__init__.py b/src/llama_store/models/__init__.py new file mode 100644 index 0000000..cbda72c --- /dev/null +++ b/src/llama_store/models/__init__.py @@ -0,0 +1,8 @@ +from .llama_id import LlamaId +from .llama import Llama +from .llama_create import LlamaCreate +from .api_token_request import ApiTokenRequest +from .api_token import ApiToken +from .user import User +from .user_registration import UserRegistration +from .llama_color import LlamaColor diff --git a/src/llama_store/models/api_token.py b/src/llama_store/models/api_token.py new file mode 100644 index 0000000..445f100 --- /dev/null +++ b/src/llama_store/models/api_token.py @@ -0,0 +1,17 @@ +from .utils.json_map import JsonMap +from .base import BaseModel + + +@JsonMap({"access_token": "accessToken", "token_type": "tokenType"}) +class ApiToken(BaseModel): + """An API token to use for authentication. + + :param access_token: The bearer token to use with the API. Pass this in the Authorization header as a bearer token. + :type access_token: str + :param token_type: The type of token. This will always be bearer., defaults to None + :type token_type: str, optional + """ + + def __init__(self, access_token: str, token_type: str = None): + self.access_token = access_token + self.token_type = token_type diff --git a/src/llama_store/models/api_token_request.py b/src/llama_store/models/api_token_request.py new file mode 100644 index 0000000..5cce437 --- /dev/null +++ b/src/llama_store/models/api_token_request.py @@ -0,0 +1,17 @@ +from .utils.json_map import JsonMap +from .base import BaseModel + + +@JsonMap({}) +class ApiTokenRequest(BaseModel): + """A request to get an API token for a given user. + + :param email: The email address of the user. This must be unique across all users. + :type email: str + :param password: The password of the user. This must be at least 8 characters long, and contain at least one letter, one number, and one special character. + :type password: str + """ + + def __init__(self, email: str, password: str): + self.email = self._pattern_matching(email, ".+\@.+\..+", "email") + self.password = password diff --git a/src/llama_store/models/base.py b/src/llama_store/models/base.py new file mode 100644 index 0000000..f62f4eb --- /dev/null +++ b/src/llama_store/models/base.py @@ -0,0 +1,272 @@ +import re +from typing import List, Union, get_origin, get_args +from enum import Enum + + +class BaseModel: + """ + A base class that most of the models in the SDK inherited from. + """ + + def __init__(self): + pass + + def _pattern_matching(self, value: str, pattern: str, variable_name: str): + """ + Checks if a value matches a regex pattern and returns the value if there's a match. + + :param value: The value to be checked. + :type value: str + :param pattern: The regex pattern. + :type pattern: str + :param variable_name: The variable name. + :type variable_name: str + :return: The value if it matches the pattern. + :rtype: str + :raises ValueError: If the value does not match the pattern. + """ + if value is None: + return None + + if re.match(r"{}".format(pattern), value): + return value + else: + raise ValueError( + f"Invalid value for {variable_name}: must match {pattern}, received {value}" + ) + + def _enum_matching( + self, value: Union[str, Enum], enum_values: List[str], variable_name: str + ): + """ + Checks if a value (str or enum) matches the required enum values and returns the value if there's a match. + + :param value: The value to be checked. + :type value: Union[str, Enum] + :param enum_values: The list of valid enum values. + :type enum_values: List[str] + :param variable_name: The variable name. + :type variable_name: str + :return: The value if it matches one of the enum values. + :rtype: Union[str, Enum] + :raises ValueError: If the value does not match any of the enum values. + """ + if value is None: + return None + + str_value = value.value if isinstance(value, Enum) else value + if str_value in enum_values: + return value + else: + raise ValueError( + f"Invalid value for {variable_name}: must match one of {enum_values}, received {value}" + ) + + def _define_object(self, input_data, input_class): + """ + Check if the input data is an instance of the input class and return the input data if it is. + Otherwise, return an instance of the input class. + + :param input_data: The input data to be checked. + :param input_class: The class that the input data should be an instance of. + :return: The input data if it is an instance of input_class, otherwise an instance of input_class. + :rtype: object + """ + if input_data is None: + return None + elif isinstance(input_data, input_class): + return input_data + else: + return input_class._unmap(input_data) + + def _define_list(self, input_data, list_class): + """ + Create a list of instances of a specified class from input data. + + :param input_data: The input data to be transformed into a list of instances. + :param list_class: The class that each instance in the list should be an instance of. + :return: A list of instances of list_class. + :rtype: list + """ + + if input_data is None: + return None + + result = [] + for item in input_data: + if hasattr(list_class, "__args__") and len(list_class.__args__) > 0: + class_list = self.__create_class_map(list_class) + OneOfBaseModel.class_list = class_list + result.append(OneOfBaseModel.return_one_of(item)) + elif issubclass(list_class, Enum): + result.append( + self._enum_matching(item, list_class.list(), list_class.__name__) + ) + elif isinstance(item, list_class): + result.append(item) + elif isinstance(item, dict): + result.append(list_class._unmap(item)) + else: + result.append(list_class(item)) + return result + + def __create_class_map(self, union_type): + """ + Create a dictionary that maps class names to the actual classes in a Union type. + + :param union_type: The Union type to create a class map for. + :return: A dictionary mapping class names to classes. + :rtype: dict + """ + class_map = {} + for arg in union_type.__args__: + if arg.__name__: + class_map[arg.__name__] = arg + return class_map + + def _get_representation(self, level=0): + """ + Get a string representation of the model. + + :param int level: The indentation level. + :return: A string representation of the model. + """ + indent = " " * level + representation_lines = [] + + for attr, value in vars(self).items(): + if value is not None: + value_representation = ( + value._get_representation(level + 1) + if hasattr(value, "_get_representation") + else repr(value) + ) + representation_lines.append( + f"{indent} {attr}={value_representation}" + ) + + return ( + f"{self.__class__.__name__}(\n" + + ",\n".join(representation_lines) + + f"\n{indent})" + ) + + def __str__(self): + return self._get_representation() + + def __repr__(self): + return self._get_representation() + + +class OneOfBaseModel: + """ + A base class for handling 'oneOf' models where multiple class constructors are available, + and the appropriate one is determined based on the input data. + + :ivar dict class_list: A dictionary mapping class names to their constructors. + """ + + class_list = {} + + @classmethod + def return_one_of(cls, input_data): + """ + Attempts to initialize an instance of one of the classes in the class_list + based on the provided input data. + + :param input_data: Input data used for initialization. + :return: An instance of one of the classes specified. + :rtype: object + :raises ValueError: If no class can be initialized with the provided input data, + or if optional parameters don't match the input data. + """ + if input_data is None: + return None + + if isinstance(input_data, (str, float, int, bool)): + return input_data + + for class_constructor in cls.class_list.values(): + exception_list = [] + try: + # Check if the class is a only a TypeHint + origin = get_origin(class_constructor) + if origin is not None and isinstance(input_data, list): + list_instance = cls._get_list_instance( + input_data, class_constructor, origin + ) + if list_instance is not None: + return list_instance + else: + continue # Try the next class constructor + + # Check if the input_data is already an instance of the class + if isinstance(input_data, class_constructor): + return input_data + + # Check if the input_data is a dictionary that can be used to initialize the class + elif isinstance(input_data, dict): + instance = class_constructor._unmap(input_data) + if cls._check_params(input_data, instance): + return instance + except Exception as e: + exception_list.append({"class": class_constructor, "exception": e}) + + cls._raise_one_of_error(exception_list) + + @classmethod + def _get_list_instance(cls, input_data, class_constructor, origin): + """ + Return the list of elements for a given class constructor and origin type. + + :param input_data: The input data to check. + :param class_constructor: The constructor of the class to check against. + :param origin: The origin type to check against. + :return: The input data if all elements are instances of the type specified in the class_constructor, + or a new list with each item unmapped. + """ + args = get_args(class_constructor) + if isinstance(input_data, origin): + if args and len(args) == 1 and isinstance(input_data, list): + inner_type = args[0] + if all(isinstance(item, inner_type) for item in input_data): + return input_data + else: + return [inner_type._unmap(item) for item in input_data] + + @classmethod + def _check_params(cls, raw_input, instance): + """ + Checks if the optional parameters in the instance match the keys in the input data, + ensuring compliance with one of the models. + + :param dict raw_input: Input data used for initialization. + :param object instance: An instance of one of the classes specified in the 'oneOf' model. + :return: True if the optional parameters in the instance match the keys in the input data, False otherwise. + """ + input_values = {k: v for k, v in raw_input.items() if v is not None}.values() + instance_map = instance._map() + instance_values = { + k: v for k, v in instance_map.items() if v is not None + }.values() + return len(input_values) == len(instance_values) + + @classmethod + def _raise_one_of_error(cls, exception_list): + """ + Raises a ValueError with the appropriate error message for one of models. + + :param exception_list: List of exceptions that occurred. + :type exception_list: list + :raises ValueError: If input data does not match any of the models. + """ + if not exception_list: + return + exception_messages = "\n".join( + f"Class: {exception['class']}, Exception: {exception['exception']}" + for exception in exception_list + ) + raise ValueError( + f"Input data must match one of the models: {list(cls.class_list.keys())}" + f"Errors occurred:\n{exception_messages}" + ) diff --git a/src/llama_store/models/llama.py b/src/llama_store/models/llama.py new file mode 100644 index 0000000..824b2bb --- /dev/null +++ b/src/llama_store/models/llama.py @@ -0,0 +1,30 @@ +from __future__ import annotations +from .utils.json_map import JsonMap +from .base import BaseModel +from .llama_color import LlamaColor + + +@JsonMap({"llama_id": "llamaId"}) +class Llama(BaseModel): + """A llama, with details of its name, age, color, and rating from 1 to 5. + + :param name: The name of the llama. This must be unique across all llamas. + :type name: str + :param age: The age of the llama in years. + :type age: int + :param color: The color of a llama. + :type color: LlamaColor + :param rating: The rating of the llama from 1 to 5. + :type rating: int + :param llama_id: The ID of the llama. + :type llama_id: int + """ + + def __init__( + self, name: str, age: int, color: LlamaColor, rating: int, llama_id: int + ): + self.name = name + self.age = age + self.color = self._enum_matching(color, LlamaColor.list(), "color") + self.rating = rating + self.llama_id = llama_id diff --git a/src/llama_store/models/llama_color.py b/src/llama_store/models/llama_color.py new file mode 100644 index 0000000..8fb26c3 --- /dev/null +++ b/src/llama_store/models/llama_color.py @@ -0,0 +1,28 @@ +from enum import Enum + + +class LlamaColor(Enum): + """An enumeration representing different categories. + + :cvar BROWN: "brown" + :vartype BROWN: str + :cvar WHITE: "white" + :vartype WHITE: str + :cvar BLACK: "black" + :vartype BLACK: str + :cvar GRAY: "gray" + :vartype GRAY: str + """ + + BROWN = "brown" + WHITE = "white" + BLACK = "black" + GRAY = "gray" + + def list(): + """Lists all category values. + + :return: A list of all category values. + :rtype: list + """ + return list(map(lambda x: x.value, LlamaColor._member_map_.values())) diff --git a/src/llama_store/models/llama_create.py b/src/llama_store/models/llama_create.py new file mode 100644 index 0000000..75262f3 --- /dev/null +++ b/src/llama_store/models/llama_create.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from .utils.json_map import JsonMap +from .base import BaseModel +from .llama_color import LlamaColor + + +@JsonMap({}) +class LlamaCreate(BaseModel): + """A new llama for the llama store. + + :param name: The name of the llama. This must be unique across all llamas. + :type name: str + :param age: The age of the llama in years. + :type age: int + :param color: The color of a llama. + :type color: LlamaColor + :param rating: The rating of the llama from 1 to 5. + :type rating: int + """ + + def __init__(self, name: str, age: int, color: LlamaColor, rating: int): + self.name = name + self.age = age + self.color = self._enum_matching(color, LlamaColor.list(), "color") + self.rating = rating diff --git a/src/llama_store/models/llama_id.py b/src/llama_store/models/llama_id.py new file mode 100644 index 0000000..dd618a0 --- /dev/null +++ b/src/llama_store/models/llama_id.py @@ -0,0 +1,14 @@ +from .utils.json_map import JsonMap +from .base import BaseModel + + +@JsonMap({"llama_id": "llamaId"}) +class LlamaId(BaseModel): + """A llama id. + + :param llama_id: The ID of the llama. + :type llama_id: int + """ + + def __init__(self, llama_id: int): + self.llama_id = llama_id diff --git a/src/llama_store/models/user.py b/src/llama_store/models/user.py new file mode 100644 index 0000000..009e6c6 --- /dev/null +++ b/src/llama_store/models/user.py @@ -0,0 +1,17 @@ +from .utils.json_map import JsonMap +from .base import BaseModel + + +@JsonMap({"id_": "id"}) +class User(BaseModel): + """A user of the llama store + + :param email: The email address of the user. This must be unique across all users. + :type email: str + :param id_: The ID of the user. This is unique across all users. + :type id_: int + """ + + def __init__(self, email: str, id_: int): + self.email = self._pattern_matching(email, ".+\@.+\..+", "email") + self.id_ = id_ diff --git a/src/llama_store/models/user_registration.py b/src/llama_store/models/user_registration.py new file mode 100644 index 0000000..43ada9f --- /dev/null +++ b/src/llama_store/models/user_registration.py @@ -0,0 +1,17 @@ +from .utils.json_map import JsonMap +from .base import BaseModel + + +@JsonMap({}) +class UserRegistration(BaseModel): + """A new user of the llama store. + + :param email: The email address of the user. This must be unique across all users. + :type email: str + :param password: The password of the user. This must be at least 8 characters long, and contain at least one letter, one number, and one special character. + :type password: str + """ + + def __init__(self, email: str, password: str): + self.email = self._pattern_matching(email, ".+\@.+\..+", "email") + self.password = password diff --git a/src/llama_store/models/utils/cast_models.py b/src/llama_store/models/utils/cast_models.py new file mode 100644 index 0000000..8535c0f --- /dev/null +++ b/src/llama_store/models/utils/cast_models.py @@ -0,0 +1,81 @@ +from enum import Enum +from typing import get_origin, get_args, Union +from inspect import isclass +from ..base import OneOfBaseModel + + +def cast_models(func): + """ + A decorator that allows for the conversion of dictionaries and enum values to model instances. + + :param func: The function to decorate. + :type func: Callable + :return: The decorated function. + :rtype: Callable + """ + + def wrapper(self, *clss, **kwargs): + cls_types = func.__annotations__ + new_cls_args = [] + new_kwargs = {} + + for input, input_type in zip(clss, cls_types.values()): + new_cls_args.append(_get_instanced_type(input, input_type)) + + for type_name, input in kwargs.items(): + new_kwargs[type_name] = _get_instanced_type(input, cls_types[type_name]) + + return func(self, *new_cls_args, **new_kwargs) + + def _get_instanced_type(data, input_type): + """ + Get instanced type based on the input data and type. + + :param data: The input data. + :param input_type: The type of the input. + :return: The instanced type. + """ + # Instanciate oneOf models + if _is_one_of_model(input_type): + class_list = { + arg.__name__: arg for arg in get_args(input_type) if arg.__name__ + } + OneOfBaseModel.class_list = class_list + return OneOfBaseModel.return_one_of(data) + + # Instanciate enum values + elif ( + isclass(input_type) + and issubclass(input_type, Enum) + and not isinstance(data, input_type) + ): + return input_type(data) + + # Instanciate object models + elif isinstance(data, dict) and input_type is not str: + return input_type(**data) + + # Instanciate list of object models + elif isinstance(data, list) and all(isinstance(i, dict) for i in data): + element_type = get_args(input_type)[0] + return [element_type(**item) for item in data] + + # Instanciate bytes if input is str + elif input_type is bytes and isinstance(data, str): + return data.encode() + + # Pass other types + else: + return data + + def _is_one_of_model(cls_type): + """ + Check if the class type is a oneOf model. + + :param cls_type: The class type to check. + :return: True if the class type is a oneOf model, False otherwise. + :rtype: bool + """ + return hasattr(cls_type, "__origin__") and cls_type.__origin__ is Union + + return wrapper diff --git a/src/llama_store/models/utils/json_map.py b/src/llama_store/models/utils/json_map.py new file mode 100644 index 0000000..aa54e65 --- /dev/null +++ b/src/llama_store/models/utils/json_map.py @@ -0,0 +1,80 @@ +from enum import Enum + + +class JsonMap: + """ + A class decorator used to map adjusted attribute names to original JSON attribute names before a request, + and vice versa after the request. + + Example: + @JsonMapping({ + 'adjusted_name': 'original_name', + 'adjusted_list': 'original_list' + }) + class SomeClass(BaseModel): + adjusted_name: str + adjusted_list: List[OtherClass] + + :param mapping: A dictionary specifying the mapping between adjusted attribute names and original JSON attribute names. + :type mapping: dict + """ + + def __init__(self, mapping): + self.mapping = mapping + + def __call__(self, cls): + """ + Transform the decorated class with attribute mapping capabilities. + + :param cls: The class to be decorated. + :type cls: type + :return: The decorated class. + :rtype: type + """ + cls.__json_mapping = self.mapping + + def _map(self): + """ + Convert the object's attributes to a dictionary with mapped attribute names. + + :return: A dictionary with mapped attribute names and values. + :rtype: dict + """ + map = self.__json_mapping + attribute_dict = vars(self) + result_dict = {} + + for key, value in attribute_dict.items(): + if isinstance(value, list): + value = [v._map() if hasattr(v, "_map") else v for v in value] + elif isinstance(value, Enum): + value = value.value + elif hasattr(value, "_map"): + value = value._map() + mapped_key = map.get(key, key) + result_dict[mapped_key] = value + + return result_dict + + @classmethod + def _unmap(cls, mapped_data): + """ + Create an object instance from a dictionary with mapped attribute names. + + :param mapped_data: A dictionary with mapped attribute names and values. + :type mapped_data: dict + :return: An instance of the class with attribute values assigned from the dictionary. + :rtype: cls + """ + reversed_map = {v: k for k, v in cls.__json_mapping.items()} + mapped_attributes = {} + for key, value in mapped_data.items(): + mapped_key = reversed_map.get(key, key) + mapped_attributes[mapped_key] = value + + return cls(**mapped_attributes) + + cls._map = _map + cls._unmap = _unmap + + return cls diff --git a/src/llama_store/net/__init__.py b/src/llama_store/net/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/net/environment/__init__.py b/src/llama_store/net/environment/__init__.py new file mode 100644 index 0000000..34d3b88 --- /dev/null +++ b/src/llama_store/net/environment/__init__.py @@ -0,0 +1 @@ +from .environment import Environment diff --git a/src/llama_store/net/environment/environment.py b/src/llama_store/net/environment/environment.py new file mode 100644 index 0000000..6a95f61 --- /dev/null +++ b/src/llama_store/net/environment/environment.py @@ -0,0 +1,11 @@ +""" +An enum class containing all the possible environments for the SDK +""" + +from enum import Enum + + +class Environment(Enum): + """The environments available for the SDK""" + + DEFAULT = "http://localhost:8080" diff --git a/src/llama_store/net/headers/__init__.py b/src/llama_store/net/headers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/net/headers/access_token_auth.py b/src/llama_store/net/headers/access_token_auth.py new file mode 100644 index 0000000..0ae3348 --- /dev/null +++ b/src/llama_store/net/headers/access_token_auth.py @@ -0,0 +1,41 @@ +from typing import Dict + +from .base_header import BaseHeader + + +class AccessTokenAuth(BaseHeader): + """ + A class for handling Access token authentication in headers. + + :ivar str _token_prefix: The prefix for the token in the header. + :ivar str token_value: The value of the token. + """ + + _token_prefix = "Bearer" + + def __init__(self, token_value: str): + """ + Initialize the AccessTokenAuth instance. + + :param token_value: The value of the token. + :type token_value: str + """ + self.token_value = token_value + + def set_value(self, value: str) -> None: + """ + Set the value of the token. + + :param value: The new value of the token. + :type value: str + """ + self.token_value = value + + def get_headers(self) -> Dict[str, str]: + """ + Get the headers with the Authorization field set to the Access token. + + :return: A dictionary with the Authorization field set to the Access token. + :rtype: Dict[str, str] + """ + return {"Authorization": f"{self._token_prefix} {self.token_value}"} diff --git a/src/llama_store/net/headers/base_header.py b/src/llama_store/net/headers/base_header.py new file mode 100644 index 0000000..cc1eb3c --- /dev/null +++ b/src/llama_store/net/headers/base_header.py @@ -0,0 +1,9 @@ +from typing import Any, Dict + + +class BaseHeader: + def set_value(self, value: Any) -> None: + pass + + def get_headers(self) -> Dict[str, str]: + pass diff --git a/src/llama_store/net/request_chain/__init__.py b/src/llama_store/net/request_chain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/net/request_chain/handlers/__init__.py b/src/llama_store/net/request_chain/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/net/request_chain/handlers/base_handler.py b/src/llama_store/net/request_chain/handlers/base_handler.py new file mode 100644 index 0000000..fb15393 --- /dev/null +++ b/src/llama_store/net/request_chain/handlers/base_handler.py @@ -0,0 +1,39 @@ +from typing import Optional, Tuple +from ...transport.request import Request +from ...transport.response import Response +from ...transport.request_error import RequestError + + +class BaseHandler: + """ + A class for sending the request through the chain of handlers. + + :ivar BaseHandler _next_handler: The next handler in the chain. + """ + + def __init__(self): + """ + Initialize a new instance of BaseHandler. + """ + self._next_handler = None + + def handle( + self, request: Request + ) -> Tuple[Optional[Response], Optional[RequestError]]: + """ + Process the given request and return a response or an error. + This method must be implemented by all subclasses. + + :param Request request: The request to handle. + :return: The response and any error that occurred. + :rtype: Tuple[Optional[Response], Optional[RequestError]] + """ + raise NotImplementedError() + + def set_next(self, handler: "BaseHandler"): + """ + Set the next handler in the chain. + + :param BaseHandler handler: The next handler. + """ + self._next_handler = handler diff --git a/src/llama_store/net/request_chain/handlers/hook_handler.py b/src/llama_store/net/request_chain/handlers/hook_handler.py new file mode 100644 index 0000000..a4bb778 --- /dev/null +++ b/src/llama_store/net/request_chain/handlers/hook_handler.py @@ -0,0 +1,47 @@ +from typing import Optional, Tuple + + +from .base_handler import BaseHandler +from ....hooks.hook import CustomHook +from ...transport.request import Request +from ...transport.response import Response +from ...transport.request_error import RequestError + + +class HookHandler(BaseHandler): + """ + Handler for calling hooks. + + :ivar Hook _hook: The hook to be called. This is a placeholder and should be replaced with an instance of the actual hook class. + """ + + def __init__(self): + """ + Initialize a new instance of HookHandler. + """ + super().__init__() + self._hook = CustomHook() + + def handle( + self, request: Request + ) -> Tuple[Optional[Response], Optional[RequestError]]: + """ + Call the beforeRequest hook before passing the request to the next handler in the chain. + Call the afterResponse hook after receiving a response from the next handler in the chain. + Call the onError hook if an error occurs in the next handler in the chain. + + :param Request request: The request to handle. + :return: The response and any error that occurred. + :rtype: Tuple[Optional[Response], Optional[RequestError]] + """ + if self._next_handler is None: + raise RequestError("Handler chain is incomplete") + + self._hook.before_request(request) + response, error = self._next_handler.handle(request) + if error is not None and error.is_http_error: + self._hook.on_error(error, request, error.response) + else: + self._hook.after_response(request, response) + + return response, error diff --git a/src/llama_store/net/request_chain/handlers/http_handler.py b/src/llama_store/net/request_chain/handlers/http_handler.py new file mode 100644 index 0000000..3b0657d --- /dev/null +++ b/src/llama_store/net/request_chain/handlers/http_handler.py @@ -0,0 +1,80 @@ +import requests + +from requests.exceptions import Timeout +from typing import Optional, Tuple +from .base_handler import BaseHandler +from ...transport.request import Request +from ...transport.response import Response +from ...transport.request_error import RequestError + + +class HttpHandler(BaseHandler): + """ + Handler for making HTTP requests. + This handler sends the request to the specified URL and returns the response. + + :ivar int _timeout_in_seconds: The timeout for the HTTP request in seconds. + """ + + def __init__(self): + """ + Initialize a new instance of HttpHandler. + """ + super().__init__() + self._timeout_in_seconds = 60 + + def handle( + self, request: Request + ) -> Tuple[Optional[Response], Optional[RequestError]]: + """ + Send the request to the specified URL and return the response. + + :param Request request: The request to send. + :return: The response and any error that occurred. + :rtype: Tuple[Optional[Response], Optional[RequestError]] + """ + try: + request_args = self._get_request_data(request) + + result = requests.request( + request.method, + request.url, + headers=request.headers, + timeout=self._timeout_in_seconds, + **request_args, + ) + response = Response(result) + + if response.status >= 400: + return None, RequestError( + message=f"{response.status} error in request to: {request.url}", + status=response.status, + response=response, + ) + + return response, None + except Timeout: + return None, RequestError("Request timed out") + + def _get_request_data(self, request: Request) -> dict: + """ + Get the request arguments based on the request headers and data. + + :param Request request: The request object. + :return: The request arguments. + :rtype: dict + """ + headers = request.headers or {} + data = request.body or {} + content_type = headers.get("Content-Type", "application/json") + + if request.method == "GET" and not data: + return {} + + if content_type.startswith("application/") and "json" in content_type: + return {"json": data} + + if "multipart/form-data" in content_type: + return {"files": data} + + return {"data": data} diff --git a/src/llama_store/net/request_chain/handlers/retry_handler.py b/src/llama_store/net/request_chain/handlers/retry_handler.py new file mode 100644 index 0000000..b497a02 --- /dev/null +++ b/src/llama_store/net/request_chain/handlers/retry_handler.py @@ -0,0 +1,65 @@ +import random + +from typing import Optional, Tuple +from time import sleep +from .base_handler import BaseHandler +from ...transport.request import Request +from ...transport.response import Response +from ...transport.request_error import RequestError + + +class RetryHandler(BaseHandler): + """ + Handler for retrying requests. + Retries the request if the previous handler in the chain returned an error or a response with a status code of 500 or higher. + + :ivar int _max_attempts: The maximum number of retry attempts. + :ivar int _delay_in_milliseconds: The delay between retry attempts in milliseconds. + """ + + def __init__(self): + """ + Initialize a new instance of RetryHandler. + """ + super().__init__() + self._max_attempts = 3 + self._delay_in_milliseconds = 150 + + def handle( + self, request: Request + ) -> Tuple[Optional[Response], Optional[RequestError]]: + """ + Retry the request if the previous handler in the chain returned an error or a response with a status code of 500 or higher. + + :param Request request: The request to retry. + :return: The response and any error that occurred. + :rtype: Tuple[Optional[Response], Optional[RequestError]] + :raises RequestError: If the handler chain is incomplete. + """ + if self._next_handler is None: + raise RequestError("Handler chain is incomplete") + + response, error = self._next_handler.handle(request) + + try_count = 0 + while try_count < self._max_attempts and self._should_retry(error): + jitter = random.uniform(0.5, 1.5) + delay = self._delay_in_milliseconds * (2**try_count) * jitter / 1000 + sleep(delay) + response, error = self._next_handler.handle(request) + try_count += 1 + + return response, error + + def _should_retry(self, error: Optional[RequestError]) -> bool: + """ + Determine whether the request should be retried. + + :param Optional[Response] response: The response from the previous handler. + :param Optional[RequestError] error: The error from the previous handler. + :return: True if the request should be retried, False otherwise. + :rtype: bool + """ + if not error: + return False + return error.status == 408 or error.status >= 500 diff --git a/src/llama_store/net/request_chain/request_chain.py b/src/llama_store/net/request_chain/request_chain.py new file mode 100644 index 0000000..363473e --- /dev/null +++ b/src/llama_store/net/request_chain/request_chain.py @@ -0,0 +1,57 @@ +from typing import Optional +from .handlers.base_handler import BaseHandler +from ..transport.request import Request +from ..transport.response import Response + + +class RequestChain: + """ + Class representing a chain of request handlers. + Handlers are added to the chain and the request is passed through each handler in the order they were added. + + :ivar Optional[BaseHandler] _head: The first handler in the chain. + :ivar Optional[BaseHandler] _tail: The last handler in the chain. + """ + + def __init__(self): + """ + Initialize a new instance of RequestChain. + """ + self._head: Optional[BaseHandler] = None + self._tail: Optional[BaseHandler] = None + + def add_handler(self, handler: BaseHandler) -> "RequestChain": + """ + Add a handler to the chain. + + :param BaseHandler handler: The handler to add. + :return: The current instance of RequestChain to allow for method chaining. + :rtype: RequestChain + """ + if self._head is None: + self._head = handler + self._tail = handler + else: + self._tail.set_next(handler) + self._tail = handler + + return self + + def send(self, request: Request) -> Response: + """ + Send the request through the chain of handlers. + + :param Request request: The request to send. + :return: The response from the request. + :rtype: Response + :raises RuntimeError: If the RequestChain is empty. + """ + if self._head is not None: + response, error = self._head.handle(request) + + if error is not None: + raise error + + return response + else: + raise RuntimeError("RequestChain is empty") diff --git a/src/llama_store/net/transport/__init__.py b/src/llama_store/net/transport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/net/transport/request.py b/src/llama_store/net/transport/request.py new file mode 100644 index 0000000..60975ff --- /dev/null +++ b/src/llama_store/net/transport/request.py @@ -0,0 +1,89 @@ +from typing import Any +from .utils import extract_original_data + + +class Request: + """ + A simple HTTP request builder class using the requests library. + + Example Usage: + ```python + # Create a Request object + request = Request() + + # Set request parameters + request.set_url('https://yourendpoint.com/') \ + .set_method('GET') \ + .set_headers({'Content-Type': 'application/json'}) \ + .set_body(None) # For GET requests, the body should be None + + # Send the HTTP request + response = request.send() + ``` + + :ivar str url: The URL of the API endpoint. + :ivar str method: The HTTP method for the request. + :ivar dict headers: Dictionary of headers to include in the request. + :ivar Any body: Request body. + """ + + def __init__(self): + self.url = None + self.method = None + self.headers = None + self.body = None + + def set_url(self, url: str) -> "Request": + """ + Set the URL of the API endpoint. + + :param str url: The URL of the API endpoint. + :return: The updated Request object. + :rtype: Request + """ + self.url = url + return self + + def set_headers(self, headers: dict) -> "Request": + """ + Set the headers for the HTTP request. + + :param dict headers: Dictionary of headers to include in the request. + :return: The updated Request object. + :rtype: Request + """ + self.headers = headers + return self + + def set_method(self, method: str) -> "Request": + """ + Set the HTTP method for the request. + + :param str method: The HTTP method (e.g., 'GET', 'POST', 'PUT', 'DELETE', etc.). + :return: The updated Request object. + :rtype: Request + """ + self.method = method + return self + + def set_body(self, body: Any, content_type: str = "application/json") -> "Request": + """ + Set the request body (e.g., JSON payload). + + :param Any body: Request body. + :param str content_type: The content type of the request body. Default is "application/json". + :return: The updated Request object. + :rtype: Request + """ + self.body = extract_original_data(body) + self.headers["Content-Type"] = content_type + return self + + def __str__(self) -> str: + """ + Return a string representation of the Request object. + + :return: A string representation of the Request object. + :rtype: str + """ + return f"Request(url={self.url}, method={self.method}, headers={self.headers}, body={self.body})" diff --git a/src/llama_store/net/transport/request_error.py b/src/llama_store/net/transport/request_error.py new file mode 100644 index 0000000..55f291b --- /dev/null +++ b/src/llama_store/net/transport/request_error.py @@ -0,0 +1,53 @@ +from .response import Response +from typing import Optional + + +class RequestError(IOError): + """ + Class representing a Request Error. + + :ivar bool is_http_error: Indicates if the error is an HTTP error. + :ivar int status: The status code of the HTTP error. + :ivar Optional[Response] response: The response associated with the error. + """ + + def __init__( + self, + message: str, + status: Optional[int] = None, + response: Optional[Response] = None, + stack: Optional["RequestError"] = None, + ): + """ + Initialize a new instance of RequestError. + + :param str message: The error message. + :param Optional[int] status: The status code of the HTTP error. + :param Optional[Response] response: The response associated with the error. + """ + super().__init__(message) + self.response = response + self.stack = stack + + if status is not None: + self.status = status + self.is_http_error = True + else: + self.status = -1 + self.is_http_error = False + + def __str__(self): + """ + Get the string representation of the error. + + :return: The string representation of the error. + :rtype: str + """ + error_stack = [] + current_error = self + while current_error is not None: + error_stack.append( + f"Error: {super().__str__()}, Status Code: {current_error.status}" + ) + current_error = current_error.stack + return "\n".join(error_stack) diff --git a/src/llama_store/net/transport/response.py b/src/llama_store/net/transport/response.py new file mode 100644 index 0000000..ac27dfa --- /dev/null +++ b/src/llama_store/net/transport/response.py @@ -0,0 +1,59 @@ +from requests import Response as RequestsResponse +from urllib.parse import parse_qs + + +class Response: + """ + A simple HTTP response wrapper class using the requests library. + + :ivar int status: The status code of the HTTP response. + :ivar dict headers: The headers of the HTTP response. + :ivar str body: The body of the HTTP response. + """ + + def __init__(self, response: RequestsResponse): + """ + Initializes a Response object. + + :param RequestsResponse response: The requests.Response object. + """ + self.status = response.status_code + self.headers = response.headers + self.body = self._get_response_body(response) + + def __str__(self) -> str: + """ + Return a string representation of the Response object. + + :return: A string representation of the Response object. + :rtype: str + """ + return ( + f"Response(status={self.status}, headers={self.headers}, body={self.body})" + ) + + def _get_response_body(self, response: RequestsResponse) -> str: + """ + Extracts the response body from a given HTTP response. + + This method attempts to parse the response body based on its content type. + If the content type is JSON, it tries to parse the body as JSON. + If the content type is text or XML, it returns the raw text. + If the content type is 'application/x-www-form-urlencoded', it parses the body as a query string. + For all other content types, it returns the raw binary content. + + :param RequestsResponse response: The HTTP response received from a request. + :return: The parsed response body. + :rtype: str or dict or bytes + """ + try: + return response.json() + except ValueError: + content_type = response.headers.get("Content-Type", "").lower() + if "text/" in content_type or content_type == "application/xml": + return response.text + elif content_type == "application/x-www-form-urlencoded": + parsed_response = parse_qs(response.text) + return {k: v[0] for k, v in parsed_response.items()} + else: + return response.content diff --git a/src/llama_store/net/transport/serializer.py b/src/llama_store/net/transport/serializer.py new file mode 100644 index 0000000..c563230 --- /dev/null +++ b/src/llama_store/net/transport/serializer.py @@ -0,0 +1,249 @@ +from typing import Any, List +from urllib.parse import quote + +from ...net.headers.base_header import BaseHeader +from .request import Request +from .utils import extract_original_data + + +class Serializer: + """ + A class for handling serialization of URL components such as headers, cookies, path parameters, and query parameters. + + :ivar str url: The base URL to be serialized. + :ivar dict[str, str] headers: A dictionary containing headers for the request. + :ivar list[str] cookies: A list containing cookie strings for the request. + :ivar dict[str, str] path: A dictionary containing path parameters for the request. + :ivar list[str] query: A list containing query parameters for the request. + """ + + def __init__(self, url: str, default_headers: List[BaseHeader] = []): + """ + Initializes a Serializer instance with the base URL. + + :param str url: The base URL to be serialized. + :param list[BaseHeader] default_headers: A list of default headers to be added to the request. Defaults to an empty list. + """ + self.url: str = url + self.headers: dict[str, str] = {} + self.cookies: list[str] = [] + self.path: dict[str, str] = {} + self.query: list[str] = [] + + for header in default_headers: + for key, value in header.get_headers().items(): + self.add_header(key, value) + + def add_header( + self, key: str, data: Any, explode: bool = False, nullable: bool = False + ) -> "Serializer": + """ + Adds a header to the request. + + :param str key: The header key. + :param Any data: The data to be serialized as the header value. + :param bool explode: Flag indicating whether to explode the data. + :return: The Serializer instance for method chaining. + :rtype: Serializer + """ + if not nullable and data is None: + return self + + data = extract_original_data(data) + + self.headers[key] = self._serialize_value( + data, explode=explode, quote_str_values=False + ) + return self + + def add_cookie( + self, key: str, data: Any, explode: bool = False, nullable: bool = False + ) -> "Serializer": + """ + Adds a cookie to the request. + + :param str key: The cookie key. + :param Any data: The data to be serialized as the cookie value. + :param bool explode: Flag indicating whether to explode the data. + :return: The Serializer instance for method chaining. + :rtype: Serializer + """ + if not nullable and data is None: + return self + + data = extract_original_data(data) + + self.cookies.append( + f"{key}={self._serialize_value(data, explode=explode, quote_str_values=False)}" + ) + return self + + def add_path( + self, + key: str, + data: Any, + explode: bool = False, + style: str = "simple", + nullable: bool = False, + ) -> "Serializer": + """ + Adds a path parameter to the request. + + :param str key: The path parameter key. + :param Any data: The data to be serialized as the path parameter value. + :param bool explode: Flag indicating whether to explode the data. + :param str style: The style of serialization for the path parameter. + :return: The Serializer instance for method chaining. + :rtype: Serializer + """ + if not nullable and data is None: + return self + + data = extract_original_data(data) + + if style == "simple": + self.path[key] = self._serialize_value(data=data, explode=explode) + elif style == "label": + separator = "." if explode else "," + self.path[key] = "." + self._serialize_value( + data=data, explode=explode, separator=separator + ) + elif style == "matrix": + separator = "," + + if isinstance(data, list) and explode: + separator = f";{key}=" + elif explode: + separator = ";" + + prefix = ";" if isinstance(data, dict) and explode else f";{key}=" + self.path[key] = prefix + self._serialize_value( + data, explode=explode, separator=separator + ) + else: + raise ValueError(f"Unsupported path style: {style}") + + return self + + def add_query( + self, + key: str, + data: Any, + explode: bool = True, + style: str = "form", + nullable: bool = False, + ) -> "Serializer": + """ + Adds a query parameter to the request. + + :param str key: The query parameter key. + :param Any data: The data to be serialized as the query parameter value. + :param bool explode: Flag indicating whether to explode the data. + :param str style: The style of serialization for the query parameter. + :return: The Serializer instance for method chaining. + :rtype: Serializer + """ + if not nullable and data is None: + return self + + data = extract_original_data(data) + + if style == "form": + separator = "&" if explode else "," + prefix = "" if (explode and isinstance(data, dict)) else f"{key}=" + query_param = f"{prefix}{self._serialize_value(data=data, explode=explode, separator=separator)}" + elif style == "spaceDelimited": + separator = f"&{key}=" if explode else f"%20" + query_param = f"{key}={self._serialize_value(data=data, explode=explode, separator=separator)}" + elif style == "pipeDelimited": + separator = f"&{key}=" if explode else "|" + query_param = f"{key}={self._serialize_value(data=data, explode=explode, separator=separator)}" + elif style == "deepObject": + query_param = "".join( + f"{key}[{k}]={quote(self._serialize_value(v))}&" + for k, v in data.items() + ).rstrip("&") + + self.query.append(query_param) + return self + + def serialize(self) -> Request: + """ + Serializes the components and returns a Request object. + + :return: The Request object containing the serialized components. + :rtype: Request + """ + final_url = self._define_url() + + if len(self.cookies) > 0: + self.headers["Cookie"] = ";".join(self.cookies) + + return Request().set_url(final_url).set_headers(self.headers) + + def _define_url(self) -> str: + """ + Constructs the final URL by replacing path parameters and appending query parameters. + + :return: The final URL. + :rtype: str + """ + final_url = self.url + + for key, value in self.path.items(): + final_url = final_url.replace(f"{{{key}}}", value) + + if len(self.query) > 0: + final_url += "?" + "&".join(self.query) + + return final_url + + def _serialize_value( + self, + data: Any, + separator: str = ",", + explode: bool = False, + quote_str_values: bool = True, + ) -> str: + """ + Serializes a value based on the specified separator and explode flag. + + :param Any data: The data to be serialized. + :param str separator: The separator used for serialization. + :param bool explode: Flag indicating whether to explode the data. + :return: The serialized value. + :rtype: str + """ + if data is None: + return "null" + + if isinstance(data, list): + return separator.join( + self._serialize_value(item, separator, explode) for item in data + ) + + if isinstance(data, dict): + if explode: + return separator.join( + [ + f"{k}={self._serialize_value(v, separator, explode)}" + for k, v in data.items() + ] + ) + else: + return separator.join( + [ + self._serialize_value(item, separator, explode) + for sublist in data.items() + for item in sublist + ] + ) + + if isinstance(data, str) and quote_str_values: + return quote(data) + if isinstance(data, bool): + return str(data).lower() + if isinstance(data, (int, float)): + return str(data) + + return data diff --git a/src/llama_store/net/transport/utils.py b/src/llama_store/net/transport/utils.py new file mode 100644 index 0000000..57febbe --- /dev/null +++ b/src/llama_store/net/transport/utils.py @@ -0,0 +1,28 @@ +from enum import Enum +from typing import Any +from ...models.base import BaseModel + + +def extract_original_data(data: Any) -> Any: + """ + Extracts the original data from internal models and enums. + + :param Any data: The data to be extracted. + :return: The extracted data. + :rtype: Any + """ + if data is None: + return None + + data_type = type(data) + + if issubclass(data_type, BaseModel): + return data._map() + + if issubclass(data_type, Enum): + return data.value + + if issubclass(data_type, list): + return [extract_original_data(item) for item in data] + + return data diff --git a/src/llama_store/sdk.py b/src/llama_store/sdk.py new file mode 100644 index 0000000..9264d4b --- /dev/null +++ b/src/llama_store/sdk.py @@ -0,0 +1,44 @@ +from .services.llama_picture import LlamaPictureService +from .services.llama import LlamaService +from .services.token import TokenService +from .services.user import UserService +from .net.environment import Environment + + +class LlamaStore: + def __init__( + self, access_token: str = None, base_url: str = Environment.DEFAULT.value + ): + """ + Initializes LlamaStore the SDK class. + """ + self.llama_picture = LlamaPictureService(base_url=base_url) + self.llama = LlamaService(base_url=base_url) + self.token = TokenService(base_url=base_url) + self.user = UserService(base_url=base_url) + self.set_access_token(access_token) + + def set_base_url(self, base_url): + """ + Sets the base URL for the entire SDK. + """ + self.llama_picture.set_base_url(base_url) + self.llama.set_base_url(base_url) + self.token.set_base_url(base_url) + self.user.set_base_url(base_url) + + return self + + def set_access_token(self, access_token: str): + """ + Sets the access token for the entire SDK. + """ + self.llama_picture.set_access_token(access_token) + self.llama.set_access_token(access_token) + self.token.set_access_token(access_token) + self.user.set_access_token(access_token) + + return self + + +# c029837e0e474b76bc487506e8799df5e3335891efe4fb02bda7a1441840310c diff --git a/src/llama_store/services/__init__.py b/src/llama_store/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/llama_store/services/llama.py b/src/llama_store/services/llama.py new file mode 100644 index 0000000..c0783b6 --- /dev/null +++ b/src/llama_store/services/llama.py @@ -0,0 +1,145 @@ +from typing import List +from .utils.validator import Validator +from .utils.base_service import BaseService +from ..net.transport.serializer import Serializer +from ..models.utils.cast_models import cast_models +from ..models.llama_create import LlamaCreate +from ..models.llama import Llama + + +class LlamaService(BaseService): + + @cast_models + def get_llamas(self) -> List[Llama]: + """Get all the llamas. + + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llamas + :rtype: List[Llama] + """ + + serialized_request = ( + Serializer(f"{self.base_url}/llama", self.get_default_headers()) + .serialize() + .set_method("GET") + ) + + response = self.send_request(serialized_request) + + return [Llama._unmap(item) for item in response] + + @cast_models + def create_llama(self, request_body: LlamaCreate) -> Llama: + """Create a new llama. Llama names must be unique. + + :param request_body: The request body. + :type request_body: LlamaCreate + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llama created successfully + :rtype: Llama + """ + + Validator(LlamaCreate).validate(request_body) + + serialized_request = ( + Serializer(f"{self.base_url}/llama", self.get_default_headers()) + .serialize() + .set_method("POST") + .set_body(request_body) + ) + + response = self.send_request(serialized_request) + + return Llama._unmap(response) + + @cast_models + def get_llama_by_id(self, llama_id: int) -> Llama: + """Get a llama by ID. + + :param llama_id: The llama's ID + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llamas + :rtype: Llama + """ + + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}", self.get_default_headers() + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("GET") + ) + + response = self.send_request(serialized_request) + + return Llama._unmap(response) + + @cast_models + def update_llama(self, request_body: LlamaCreate, llama_id: int) -> Llama: + """Update a llama. If the llama does not exist, create it. + + When updating a llama, the llama name must be unique. If the llama name is not unique, a 409 will be returned. + + :param request_body: The request body. + :type request_body: LlamaCreate + :param llama_id: The llama's ID + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Successful Response + :rtype: Llama + """ + + Validator(LlamaCreate).validate(request_body) + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}", self.get_default_headers() + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("PUT") + .set_body(request_body) + ) + + response = self.send_request(serialized_request) + + return Llama._unmap(response) + + @cast_models + def delete_llama(self, llama_id: int): + """Delete a llama. If the llama does not exist, this will return a 404. + + :param llama_id: The llama's ID + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + """ + + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}", self.get_default_headers() + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("DELETE") + ) + + response = self.send_request(serialized_request) + + return response diff --git a/src/llama_store/services/llama_picture.py b/src/llama_store/services/llama_picture.py new file mode 100644 index 0000000..c9100fc --- /dev/null +++ b/src/llama_store/services/llama_picture.py @@ -0,0 +1,138 @@ +from .utils.validator import Validator +from .utils.base_service import BaseService +from ..net.transport.serializer import Serializer +from ..models.utils.cast_models import cast_models +from ..models.llama_id import LlamaId + + +class LlamaPictureService(BaseService): + + @cast_models + def get_llama_picture_by_llama_id(self, llama_id: int) -> bytes: + """Get a llama's picture by the llama ID. Pictures are in PNG format. + + :param llama_id: The ID of the llama to get the picture for + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llamas + :rtype: bytes + """ + + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}/picture", + self.get_default_headers(), + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("GET") + ) + + response = self.send_request(serialized_request) + + return response + + @cast_models + def create_llama_picture( + self, llama_id: int, request_body: bytes = None + ) -> LlamaId: + """Create a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. + + :param request_body: The request body., defaults to None + :type request_body: bytes, optional + :param llama_id: The ID of the llama that this picture is for + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llama picture created successfully + :rtype: LlamaId + """ + + Validator(bytes).is_optional().validate(request_body) + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}/picture", + self.get_default_headers(), + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("POST") + .set_body(request_body, "image/png") + ) + + response = self.send_request(serialized_request) + + return LlamaId._unmap(response) + + @cast_models + def update_llama_picture( + self, llama_id: int, request_body: bytes = None + ) -> LlamaId: + """Update a picture for a llama. The picture is sent as a PNG as binary data in the body of the request. + + If the llama does not have a picture, one will be created. If the llama already has a picture, + it will be overwritten. + If the llama does not exist, a 404 will be returned. + + :param request_body: The request body., defaults to None + :type request_body: bytes, optional + :param llama_id: The ID of the llama that this picture is for + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: Llama picture created successfully + :rtype: LlamaId + """ + + Validator(bytes).is_optional().validate(request_body) + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}/picture", + self.get_default_headers(), + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("PUT") + .set_body(request_body, "image/png") + ) + + response = self.send_request(serialized_request) + + return LlamaId._unmap(response) + + @cast_models + def delete_llama_picture(self, llama_id: int): + """Delete a llama's picture by ID. + + :param llama_id: The ID of the llama to delete the picture for + :type llama_id: int + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + """ + + Validator(int).validate(llama_id) + + serialized_request = ( + Serializer( + f"{self.base_url}/llama/{{llama_id}}/picture", + self.get_default_headers(), + ) + .add_path("llama_id", llama_id) + .serialize() + .set_method("DELETE") + ) + + response = self.send_request(serialized_request) + + return response diff --git a/src/llama_store/services/token.py b/src/llama_store/services/token.py new file mode 100644 index 0000000..a68f6bb --- /dev/null +++ b/src/llama_store/services/token.py @@ -0,0 +1,37 @@ +from .utils.validator import Validator +from .utils.base_service import BaseService +from ..net.transport.serializer import Serializer +from ..models.utils.cast_models import cast_models +from ..models.api_token_request import ApiTokenRequest +from ..models.api_token import ApiToken + + +class TokenService(BaseService): + + @cast_models + def create_api_token(self, request_body: ApiTokenRequest) -> ApiToken: + """Create an API token for a user. These tokens expire after 30 minutes. + + Once you have this token, you need to pass it to other endpoints in the Authorization header as a Bearer token. + + :param request_body: The request body. + :type request_body: ApiTokenRequest + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: A new API token for the user + :rtype: ApiToken + """ + + Validator(ApiTokenRequest).validate(request_body) + + serialized_request = ( + Serializer(f"{self.base_url}/token", self.get_default_headers()) + .serialize() + .set_method("POST") + .set_body(request_body) + ) + + response = self.send_request(serialized_request) + + return ApiToken._unmap(response) diff --git a/src/llama_store/services/user.py b/src/llama_store/services/user.py new file mode 100644 index 0000000..8faa454 --- /dev/null +++ b/src/llama_store/services/user.py @@ -0,0 +1,67 @@ +from .utils.validator import Validator +from .utils.base_service import BaseService +from ..net.transport.serializer import Serializer +from ..models.utils.cast_models import cast_models +from ..models.user_registration import UserRegistration +from ..models.user import User + + +class UserService(BaseService): + + @cast_models + def get_user_by_email(self, email: str) -> User: + """Get a user by email. + + This endpoint will return a 404 if the user does not exist. Otherwise, it will return a 200. + + :param email: The user's email address + :type email: str + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: User + :rtype: User + """ + + Validator(str).min_length(5).max_length(254).pattern(".+\@.+\..+").validate( + email + ) + + serialized_request = ( + Serializer(f"{self.base_url}/user/{{email}}", self.get_default_headers()) + .add_path("email", email) + .serialize() + .set_method("GET") + ) + + response = self.send_request(serialized_request) + + return User._unmap(response) + + @cast_models + def register_user(self, request_body: UserRegistration) -> User: + """Register a new user. + + This endpoint will return a 400 if the user already exists. Otherwise, it will return a 201. + + :param request_body: The request body. + :type request_body: UserRegistration + ... + :raises RequestError: Raised when a request fails, with optional HTTP status code and details. + ... + :return: User registered successfully + :rtype: User + """ + + Validator(UserRegistration).validate(request_body) + + serialized_request = ( + Serializer(f"{self.base_url}/user", self.get_default_headers()) + .serialize() + .set_method("POST") + .set_body(request_body) + ) + + response = self.send_request(serialized_request) + + return User._unmap(response) diff --git a/src/llama_store/services/utils/base_service.py b/src/llama_store/services/utils/base_service.py new file mode 100644 index 0000000..810aead --- /dev/null +++ b/src/llama_store/services/utils/base_service.py @@ -0,0 +1,74 @@ +from typing import Any, Dict +from enum import Enum + +from .default_headers import DefaultHeaders, DefaultHeadersKeys +from ...net.transport.request import Request +from ...net.request_chain.request_chain import RequestChain +from ...net.request_chain.handlers.hook_handler import HookHandler +from ...net.request_chain.handlers.http_handler import HttpHandler +from ...net.headers.access_token_auth import AccessTokenAuth +from ...net.request_chain.handlers.retry_handler import RetryHandler + + +class BaseService: + """ + A base class for services providing common functionality. + + :ivar str base_url: The base URL for the service. + :ivar dict _default_headers: A dictionary of default headers. + """ + + def __init__(self, base_url: str) -> None: + """ + Initializes a BaseService instance. + + :param str base_url: The base URL for the service. Defaults to None. + """ + self.base_url = base_url + self._default_headers = DefaultHeaders() + self._request_handler = ( + RequestChain() + .add_handler(RetryHandler()) + .add_handler(HookHandler()) + .add_handler(HttpHandler()) + ) + + def set_access_token(self, access_token: str): + """ + Sets the access token for the entire SDK. + """ + self._default_headers.set_header( + DefaultHeadersKeys.ACCESS_AUTH, AccessTokenAuth(access_token) + ) + + return self + + def set_base_url(self, base_url: str): + """ + Sets the base URL for the service. + + :param str base_url: The base URL to be set. + """ + self.base_url = base_url + + return self + + def send_request(self, request: Request) -> dict: + """ + Sends the given request. + + :param Request request: The request to be sent. + :return: The response data. + :rtype: dict + """ + response = self._request_handler.send(request) + return response.body + + def get_default_headers(self) -> list: + """ + Get the default headers. + + :return: A list of the default headers. + :rtype: list + """ + return self._default_headers.get_headers() diff --git a/src/llama_store/services/utils/default_headers.py b/src/llama_store/services/utils/default_headers.py new file mode 100644 index 0000000..69cc8fa --- /dev/null +++ b/src/llama_store/services/utils/default_headers.py @@ -0,0 +1,57 @@ +from enum import Enum +from typing import Any, Dict + +from ...net.headers.base_header import BaseHeader + + +class DefaultHeadersKeys(Enum): + """ + An enumeration of default headers. + + :ivar str ACCESS_AUTH: The access token authentication header. + :ivar str BASIC_AUTH: The basic authentication header. + :ivar str API_KEY_AUTH: The API key authentication header. + """ + + ACCESS_AUTH = "access_token_auth" + BASIC_AUTH = "basic_auth" + API_KEY_AUTH = "api_key_auth" + + +class DefaultHeaders: + """ + A class to manage default headers. + + :ivar Dict[str, BaseHeader] _default_headers: The default headers. + """ + + def __init__(self): + self._default_headers: Dict[str, BaseHeader] = {} + + def set_header(self, key: DefaultHeadersKeys, value: BaseHeader) -> None: + """ + Set a default header. + + :param DefaultHeadersKeys key: The key of the header to set. + :param Any value: The value to set for the header. + """ + self._default_headers[key.value] = value + + def get_header(self, key: DefaultHeadersKeys) -> BaseHeader: + """ + Get a default header. + + :param DefaultHeadersKeys key: The key of the header to get. + :return: The value of the header. + :rtype: Any + """ + return self._default_headers[key.value] + + def get_headers(self) -> list: + """ + Get the default headers. + + :return: A list of the default headers. + :rtype: list + """ + return list(self._default_headers.values()) diff --git a/src/llama_store/services/utils/validator.py b/src/llama_store/services/utils/validator.py new file mode 100644 index 0000000..9b47026 --- /dev/null +++ b/src/llama_store/services/utils/validator.py @@ -0,0 +1,216 @@ +import re +import operator +from typing import Union, Any, Type, Pattern, get_args +from ...models.base import OneOfBaseModel + + +class Validator: + """ + A simple validator class for validating the type and pattern of a value. + + :ivar Type[Any] _type: The expected type for the value. + :ivar bool _is_optional: Flag indicating whether the value is optional. + :ivar bool _is_array: Flag indicating whether the value is an array. + :ivar Pattern[str] _pattern: The regular expression pattern for validating the value. + :ivar int _min: The minimum value for validating the value. + :ivar int _max: The maximum value for validating the value. + """ + + def __init__(self, _type: Type[Any] = None): + """ + Initializes a Validator instance. + + :param Type[Any] _type: The expected type for the value. Defaults to None. + """ + self._type: Type[Any] = _type + self._is_optional: bool = False + self._is_array: bool = False + self._pattern: Pattern[str] = None + self._min_length: int = None + self._max_length: int = None + self._min: int = None + self._min_exclusive: bool = False + self._max: int = None + self._max_exclusive: bool = False + + def is_array(self) -> "Validator": + """ + Marks the value as an array. + + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._is_array = True + return self + + def is_optional(self) -> "Validator": + """ + Marks the value as optional. + + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._is_optional = True + return self + + def pattern(self, pattern: str) -> "Validator": + """ + Specifies a regular expression pattern for validating the value. + + :param str pattern: The regular expression pattern. + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._pattern = re.compile(pattern) + return self + + def min(self, min: int, exclusive=False) -> "Validator": + """ + Specifies a minimum value for validating the value. + + :param int min: The minimum value to be validated against. + :param bool exclusive: (optional) If set to True, the minimum value is not inclusive. + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._min = min + self._min_exclusive = exclusive + return self + + def max(self, max: int, exclusive=False) -> "Validator": + """ + Specifies a maximum value for validating the value. + + :param int max: The maximum value. + :param bool exclusive: (optional) If set to True, the maximum value is not inclusive. + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._max = max + self._max_exclusive = exclusive + return self + + def min_length(self, min_length: int) -> "Validator": + """ + Specifies a minimum length for validating the value. + + :param int min_length: The minimum length to be validated against. + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._min_length = min_length + return self + + def max_length(self, max_length: int) -> "Validator": + """ + Specifies a maximum length for validating the value. + + :param int max_length: The maximum length. + :return: The Validator instance for method chaining. + :rtype: Validator + """ + self._max_length = max_length + return self + + def validate(self, value: Any) -> None: + """ + Validates the provided value based on the specified criteria. + + :param Any value: The input that needs to be checked + :raises ValueError: If the value does not meet the specified validation criteria. + """ + if not self._type: + raise TypeError("Invalid type: No type specified") + if self._is_optional and value is None: + return + + self._validate_type(value) + self._validate_rules(value) + + def _validate_type(self, value: Any) -> None: + """ + Validates the type of the value. + + :param Any value: The input that needs to be checked + :raises ValueError: If the value does not meet the expected type. + """ + if self._is_one_of_type(self._type): + self._validate_one_of_type(value) + elif self._is_array: + self._validate_array_type(value) + elif not self._match_type(value): + raise TypeError(f"Invalid type: Expected {self._type}, got {type(value)}") + + def _validate_one_of_type(self, value: Any) -> None: + """ + Validates oneOf model type. + + :param Any value: The input that needs to be checked + :raises ValueError: If the value does not match the oneOf rules. + """ + class_list = {arg.__name__: arg for arg in get_args(self._type) if arg.__name__} + OneOfBaseModel.class_list = class_list + OneOfBaseModel.return_one_of(value) + + def _validate_array_type(self, value: Any) -> None: + """ + Validates the type of an array value. + + :param Any value: The input that needs to be checked + :raises ValueError: If the array items do not match the expected type. + """ + if any(self._match_type(v) is False for v in value): + raise TypeError(f"Invalid type: Expected {self._type}, got {type(value)}") + + def _match_type(self, value: Any) -> bool: + """ + Checks if the value matches the expected type. + + :param Any value: The input that needs to be checked + :raises ValueError: If the value does not match the expected type. + """ + is_numeric = self._type is float and isinstance(value, int) + if isinstance(value, self._type) or is_numeric: + return True + return False + + def _validate_rules(self, value: Any) -> None: + """ + Validate the rules specified for the value. + + :param Any value: The input that needs to be validated + :raises ValueError: If the value does not meet the specified validation criteria. + """ + min_operator = operator.lt if self._min_exclusive else operator.le + max_operator = operator.gt if self._max_exclusive else operator.ge + + if self._min is not None and not min_operator(self._min, value): + raise ValueError( + f"Invalid value: {value} is {'less than or equal to' if self._min_exclusive else 'less than'} {self._min}" + ) + if self._max is not None and not max_operator(self._max, value): + raise ValueError( + f"Invalid value: {value} is {'greater than or equal to' if self._max_exclusive else 'greater than'} {self._max}" + ) + if self._min_length is not None and len(value) < self._min_length: + raise ValueError( + f"Invalid value: the length of {value} is less than {self._min_length}" + ) + if self._max_length is not None and len(value) > self._max_length: + raise ValueError( + f"Invalid value: the length of {value} is greater than {self._max_length}" + ) + if self._pattern and not self._pattern.match(str(value)): + raise ValueError( + f"Invalid value: {value} does not match pattern {self._pattern}" + ) + + def _is_one_of_type(self, cls_type): + """ + Checks if the provided type is a Union type. + + :param Type[Any] cls_type: The type to be checked. + :return: True if the type is a Union type, False otherwise. + :rtype: bool + """ + return hasattr(cls_type, "__origin__") and cls_type.__origin__ is Union