User Guide¶
Installation¶
This part of the documentation covers the installation of CRUDs.
From PyPI¶
To install a stable version of CRUDs, use pip:
$ python -m pip install cruds
If you have installed Python, then pip should be available. If not visit getting-started with pip.
From Source¶
If you would like to install the latest unreleased source code you can clone it from Github CRUDs repository.
Using a git client you can clone the repository and install it with pip:
$ git clone https://github.com/johnbrandborg/cruds.git
$ python -m pip install ./cruds
For developers wanting to contribute, please visit the Development section.
General Usage¶
Unlike other HTTP packages that require the full URL and methods as arguments with every request, with CRUDs the object you create is a representation of a platform interface. In its most basic form only the host name is required when creating your client instance.
from cruds import Client
api = Client("https://host/")
When creating the client all features can be adjusted to suit most needs. Refer
to the module contents of cruds.Client for more information.
# Disable retries and set the required timeout to 20 seconds
api = Client("https://host/", retries=0, timeout=20)
# Send & receive raw data and never raise an exception on bad status codes
api = Client("https://host/", serialize=False, raise_status=False)
# Disable SSL Verification
api = Client("https://host/", verify_ssl=False)
By default CRUDs will raise an exception if it is not able to give you your data. While uncommon if required to ignore a status code raising an exception you can do this by adding and removing that status code into the ignore set:
api.status_ignore.add(409)
api.status_ignore.remove(409)
Once the client has been created, CRUD requests can be made by supplying URI’s, data & params with Dictionaries.
Example
user = "/api/v1/user"
# Create a User
api.create(user, data={"name": "fred"}, params={"company_id": "1003"})
# Read User details
fred = api.read(user, params={"name": "fred", "select": "id"})
# Update the User details
api.update(f"{user}/{fred}", data={"name": "Fred"})
# Delete the User
api.delete(f"{user}/{fred}")
While most HTTP clients require you to handle web response objects and deal with issues, retries, and data extraction, our CRUD Client methods simplify the process by only returning the necessary data. In the event of a request issue, an error will be raised, ensuring a more efficient and streamlined experience.
Comparison with Other Libraries¶
Here are detailed comparisons showing how CRUDs simplifies common API tasks compared to other popular HTTP libraries.
Basic API Call
import cruds
api = cruds.Client("https://api.example.com")
user = api.read("users/123")
import requests
response = requests.get("https://api.example.com/users/123")
response.raise_for_status()
user = response.json()
import httpx
with httpx.Client() as client:
response = client.get("https://api.example.com/users/123")
response.raise_for_status()
user = response.json()
Authentication
import cruds
api = cruds.Client("https://api.example.com", auth="your-token")
users = api.read("users")
import requests
headers = {"Authorization": "Bearer your-token"}
response = requests.get("https://api.example.com/users", headers=headers)
OAuth2 Authentication
import cruds
from cruds.auth import OAuth2
api = cruds.Client(
"https://api.example.com",
auth=OAuth2(
url="https://api.example.com/oauth/token",
client_id="your-client-id",
client_secret="your-client-secret",
scope="read write"
)
)
users = api.read("users")
import requests
from requests_oauthlib import OAuth2Session
oauth = OAuth2Session(client_id, redirect_uri=redirect_uri)
authorization_url, state = oauth.authorization_url(auth_url)
# ... handle redirect, get code, exchange for token
token = oauth.fetch_token(token_url, client_secret=client_secret)
response = requests.get(
"https://api.example.com/users",
headers={"Authorization": f"Bearer {token['access_token']}"}
)
Error Handling and Retries
import cruds
api = cruds.Client("https://api.example.com", retries=3)
users = api.read("users")
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
try:
response = session.get("https://api.example.com/users")
response.raise_for_status()
users = response.json()
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
Full CRUD Operations
import cruds
api = cruds.Client("https://api.example.com")
user = api.create("users", data={"name": "John", "email": "john@example.com"})
api.update(f"users/{user['id']}", data={"name": "John Doe"})
api.delete(f"users/{user['id']}")
import requests
user_data = {"name": "John", "email": "john@example.com"}
response = requests.post("https://api.example.com/users", json=user_data)
response.raise_for_status()
user = response.json()
update_data = {"name": "John Doe"}
response = requests.patch(
f"https://api.example.com/users/{user['id']}", json=update_data
)
response.raise_for_status()
response = requests.delete(f"https://api.example.com/users/{user['id']}")
response.raise_for_status()
Working with Query Parameters
import cruds
api = cruds.Client("https://api.example.com")
users = api.read("users", params={
"page": 1,
"limit": 10,
"filter": "active",
"sort": "name"
})
import requests
from urllib.parse import urlencode
params = {
"page": 1,
"limit": 10,
"filter": "active",
"sort": "name"
}
response = requests.get(
f"https://api.example.com/users?{urlencode(params)}"
)
Method Relationship¶
To make it easier to understand how to use CRUD operations, here is a breakdown
of the relevant web method requests using the Client Class methods. While they
are closely related, there is a minor difference to be aware of. Generally the
relation is one to one with the exception being update.
By default update will use a PATCH method which generally indicates only updating
the set of specific values. An update may also use the PUT method to perform a
replacement, which can be used by setting replace to True.
Client Method |
HTTP Method |
|---|---|
create() |
POST |
read() |
GET |
update() |
PATCH |
update(replace=True) |
PUT |
delete() |
DELETE |
Authentication¶
When authenticating with the Client, the Auth argument will detect how you want to authenticate. If you don’t use the Auth argument no authentication is used.
If you supply only a string it will be used as a bearer token. A list or tuple will be used for Username and Password, and lastly an Auth Class is a complex Workflow. (eg, See OAuth2 below)
from cruds import Client
# Authentication with Token
api = Client("https://host/", auth="bearer-token")
# Authentication with Username and Password
api = Client("https://host/", auth=("username", "password"))
OAuth2 Workflows¶
OAuth 2 is the industry-standard protocol for authorization. CRUDs supports the Authorization Flows:
Client Credentials
Resource Owner Password (if username and password arguments are supplied)
Authorization Code (with state parameter for CSRF protection)
When an expiry time is returned by the server with the access token refreshing is taken care of automatically, along with using refresh tokens.
Security Features:
Token Encryption: All token state is encrypted in memory using Fernet encryption
Custom Encryption Keys: You can provide your own encryption key for enhanced security
Automatic Key Derivation: If no custom key is provided, a key is derived from the client_secret
State Parameter: CSRF protection for Authorization Code flow using cryptographically secure state parameters
from cruds import Client
from cruds.auth import OAuth2
# Basic OAuth2 with automatic key derivation
api = Client(
host="https://host/",
auth=OAuth2(
url="https://host/token",
client_id="id",
client_secret="secret",
scope="all-apis",
# Rich Authorization Requests (RAR)
authorization_details=[
{
"type": "permissions",
"operation": "read",
}
]
)
)
# OAuth2 with custom encryption key (recommended for production)
api = Client(
host="https://host/",
auth=OAuth2(
url="https://host/token",
client_id="id",
client_secret="secret",
scope="all-apis",
encryption_key="your-32-character-encryption-key-here"
)
)
# Authorization Code flow with state parameter (most secure for user-facing apps)
oauth = OAuth2(
url="https://host/token",
client_id="id",
client_secret="secret",
scope="all-apis",
authorization_url="https://host/authorize",
redirect_uri="https://your-app.com/callback",
encryption_key="your-32-character-encryption-key-here"
)
# Step 1: Generate authorization URL with state parameter
auth_url = oauth.get_authorization_url()
# Redirect user to auth_url
# Step 2: After user authorization, parse the redirect response
redirect_url = "https://your-app.com/callback?code=abc123&state=xyz789"
code, state = oauth.parse_authorization_response(redirect_url)
# Step 3: Exchange authorization code for access token
access_token = oauth.exchange_code_for_token(code, state)
# Use the OAuth2 instance with the Client
api = Client(host="https://host/", auth=oauth)
Note
The OAuth 2.0 framework will take time to implement and implemented properly. Support in improving this coverage is very welcome. Let the project know of any Issues.
Note
For production environments, it’s recommended to provide a custom encryption_key rather than relying on automatic key derivation from client_secret.
Note
The Authorization Code flow with state parameter is the most secure OAuth flow for user-facing applications, providing CSRF protection and following OAuth 2.0 security best practices.
Serialize¶
By default the Client of the API will attempt to Serialize and Deserialize JSON into and from Python built-in data types. Lists and Dictionaries with basic data types like boolean, floats, integers, and strings can be on the data argument, and the returned data is the same.
The API however needs to indicate the content type is JSON! If not the Client will
attempt to return JSON, and will fall-back to returning the bytes type data if
deserialization fails, presuming that serialize is enabled by mistake.
If the Client has serialization disabled only the string or byte types are taken as data, and the return is bytes type data.
Note
If there is a need to expand on the SerDes content types, please raise a issue in the Github repository so the project is aware of it.
Multipart File Uploads¶
The create() and update() methods support multipart file uploads via the
files parameter. When provided, the request is sent as multipart/form-data
using urllib3’s fields parameter — no manual encoding is needed.
The files dictionary values follow urllib3’s field tuple format:
(filename, data)— file upload with auto-detected MIME type(filename, data, content_type)— file upload with explicit MIME typestrorbytes— plain form field (for mixing form data with files)
import cruds
api = cruds.Client("https://api.example.com", auth="your-token")
# Upload a CSV file
api.create(
"upload/endpoint",
data=None,
files={"file": ("data.csv", csv_bytes, "text/csv")},
)
# Upload without specifying MIME type (auto-detected)
api.create(
"upload/endpoint",
data=None,
files={"file": ("report.pdf", pdf_bytes)},
)
# Mix form fields with file uploads
api.create(
"upload/endpoint",
data=None,
files={
"description": "Monthly report",
"file": ("report.pdf", pdf_bytes, "application/pdf"),
},
)
When files is provided it takes precedence over data and the serialize
setting. When files is not provided (the default), the existing behaviour is
unchanged.
The update() method works the same way:
api.update(
"documents/123",
data=None,
files={"file": ("updated.csv", new_csv_bytes, "text/csv")},
)
Retries¶
Connections, reads, redirects, and bad status codes are implemented in the CRUDs Client to perform retries. If any of these are encounted a retry will take place a total of 4 times across the different types. Each retry will also backoff by a factor of 0.9 after the second attempt. This can give the communication between the client and server time to get established or recover from being rate limited.
Status Codes:
408 Request Timeout
425 Too Early
429 Too Many Requests (Rate Limited)
500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
You can adjust the retries, backoff_factor and retry_status_codes on the Client with arguments. If you increase the retries, consider reducing the backoff amount to avoid large delays, however no backoff will ever be longer the maximum of 120 seconds.
Logging¶
Because CRUDs is high level it has verbose logging to assist with capturing information around general operations.
If you want to see logging set the level to INFO using the logging module as shown, below before you create the Client instance.
import logging
logging.basicConfig(level=logging.INFO)
Setting the level to logging.DEBUG will show you URLLib3 messages which is
a useful way to see what calls CRUDs makes to URLLib3.
Extensibility¶
The library has been created with extensibility in mind, so that Software Development Kits can be created. There are two ways that this can be done, one as described below or by creating an ‘Interface As Code’.
The basic approach is to create a new subclass and add the logic requirements needed to make the requests. You are effectively just adding the host name into the initialization and the URI into the methods:
from cruds import Client
class CatFactNinja(Client):
"""Cat Fact Ninja Interface"""
_fact_uri = "fact"
def __init__(self, **kwargs):
super().__init__("http://catfact.ninja/", **kwargs)
@property
def fact(self):
""" Get a Fact about Cats"""
return self.read(self._fact_uri).get("fact")
cat = CatFactNinja()
print(cat.fact)
Interfaces
CRUDs also supports creating interfaces (basically SDKs) with large amounts of models as a mixture of YAML configuration and functions for the common logic. This significantly reduces the amount of python coding needed, and the common methods can be reused.
For more information on Interfaces that come with CRUDs and how to create them visit the Interfaces page.