Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to PDM and Ruff #20

Merged
merged 8 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Build

on:
pull_request:
types: [ opened, synchronize, reopened ]
push:
branches:
- main

jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.10', '3.11']
os: [ubuntu-latest, macOS-latest, windows-latest]

steps:
- uses: actions/checkout@v4
- name: Set up PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pdm sync -d -G dev
- name: Run Lint
run: |
pdm lint
- name: Run Tests
run: |
pdm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ venv/
.tox/
.cache/
.coverage
.pdm-python

38 changes: 38 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
default_language_version:
python: python3.11

repos:
# - repo: https://github.com/jorisroovers/gitlint
# rev: v0.19.1
# hooks:
# - id: gitlint
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-merge-conflict
- id: debug-statements
- id: trailing-whitespace
- id: check-case-conflict
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-json
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix=lf]
- id: name-tests-test
exclude: '/(helpers\.py|base\.py)'
args: [--django]

- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.4
hooks:
- id: forbid-crlf

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
args: [ "--config", "pyproject.toml" ]
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
### v0.11.2 (2024-07-28)

* Format all files with Ruff

### v0.11.1 (2024-07-28)

* Switch to Ruff and cleanup files

### v0.11.0 (2024-01-12)

* Refactor to use sessions to handle retries.
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class MyClass(Main):
'USE_DASHES': False,
"SESSION_TRIES": 3,
'SESSION_TIMEOUT': None,
'SESSION_VERIFY': False,
'SESSION_VERIFY': False,
}

export DRF_CLIENT_AUTH_TOKEN=1fe171f65917db0072abc6880196989dd2a20025
Expand Down Expand Up @@ -201,10 +201,15 @@ To test, run python setup.py test or to run coverage analysis:
```bash
python3 -m venv .virtualenv/drf_client
source .virtualenv/drf_client/bin/activate
pip install -r requirements-test.txt
pip install -e .
pip install pdm
pdm install

py.test
pdm run test

# Install pre-commit hooks
pre-commit install
pre-commit install --hook-type prepare-commit-msg
pre-commit install --hook-type commit-msg
```

## CI Deployment
Expand All @@ -223,10 +228,7 @@ git push --tags
## Manual Deployment

```bash
pip install -r requirements-build.txt

python setup.py sdist bdist_wheel
twine check dist/*
pdm build
# Publish
twine upload dist/*
```
91 changes: 35 additions & 56 deletions drf_client/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@
obj_one = api.some_model(1).get()
api.logout()
"""

import json
import logging

import requests

from .exceptions import (
HttpNotFoundError,
HttpClientError,
HttpServerError,
HttpNotFoundError,
HttpClientError,
HttpServerError,
RestBaseException,
HttpCouldNotVerifyServerError,
)
Expand Down Expand Up @@ -61,8 +62,8 @@

class RestResource:
"""
Resource provides the main functionality behind a Django Rest Framework based API.
It handles the attribute -> url, kwarg -> query param, and other related behind the
Resource provides the main functionality behind a Django Rest Framework based API.
It handles the attribute -> url, kwarg -> query param, and other related behind the
scenes python to HTTP transformations. It's goal is to represent a single resource
which may or may not have children.
"""
Expand All @@ -72,7 +73,7 @@ class RestResource:

def __init__(self, *args, **kwargs):
self._store = kwargs
self._session = kwargs.get('session')
self._session = kwargs.get("session")

if self._session is None:
self._session = requests.Session()
Expand Down Expand Up @@ -132,9 +133,7 @@ def _copy_kwargs(self, dictionary: dict) -> dict:

def _check_for_errors(self, resp, url):
if 400 <= resp.status_code <= 499:
exception_class = (
HttpNotFoundError if resp.status_code == 404 else HttpClientError
)
exception_class = HttpNotFoundError if resp.status_code == 404 else HttpClientError
raise exception_class(
"Client Error %s: %s" % (resp.status_code, url),
response=resp,
Expand All @@ -157,7 +156,7 @@ def _try_to_serialize_response(self, resp):
return

if resp.content:
if type(resp.content) == bytes:
if isinstance(resp.content, bytes):
try:
encoding = requests.utils.guess_json_utf(resp.content)
return json.loads(resp.content.decode(encoding))
Expand Down Expand Up @@ -186,9 +185,7 @@ def _get_headers(self):
if self._store["use_token"]:
if "token" not in self._store:
raise RestBaseException("No Token")
authorization_str = self._options["TOKEN_FORMAT"].format(
token=self._store["token"]
)
authorization_str = self._options["TOKEN_FORMAT"].format(token=self._store["token"])
headers["Authorization"] = authorization_str

return headers
Expand All @@ -198,11 +195,7 @@ def raw_get(self, extra_headers: dict = None, **kwargs):
args = None
if "extra" in kwargs:
args = kwargs["extra"]
headers = (
self._get_headers() | extra_headers
if extra_headers
else self._get_headers()
)
headers = self._get_headers() | extra_headers if extra_headers else self._get_headers()

return self._session.get(self.url(args), headers=headers)

Expand All @@ -214,16 +207,13 @@ def get(self, extra_headers: dict = None, **kwargs):
def raw_post(self, data: dict = None, extra_headers: dict = None, **kwargs):
"""Call requests post and return raw respond."""
payload = json.dumps(data) if data and "files" not in kwargs else data
headers = (
self._get_headers() | extra_headers
if extra_headers
else self._get_headers()
)
headers = self._get_headers() | extra_headers if extra_headers else self._get_headers()
try:
resp = self._session.post(self.url(), data=payload, headers=headers, **kwargs)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
return resp

Expand All @@ -235,17 +225,14 @@ def post(self, data: dict = None, extra_headers: dict = None, **kwargs):
def raw_patch(self, data=None, extra_headers: dict = None, **kwargs):
"""Call patch and return raw request respond."""
payload = json.dumps(data) if data and "files" not in kwargs else data
headers = (
self._get_headers() | extra_headers
if extra_headers
else self._get_headers()
)
headers = self._get_headers() | extra_headers if extra_headers else self._get_headers()

try:
resp = self._session.patch(self.url(), data=payload, headers=headers, **kwargs)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
return resp

Expand All @@ -257,17 +244,14 @@ def patch(self, data=None, extra_headers: dict = None, **kwargs):
def raw_put(self, data=None, extra_headers: dict = None, **kwargs):
"""Call Put and return raw request respond."""
payload = json.dumps(data) if data and "files" not in kwargs else data
headers = (
self._get_headers() | extra_headers
if extra_headers
else self._get_headers()
)
headers = self._get_headers() | extra_headers if extra_headers else self._get_headers()

try:
resp = self._session.put(self.url(), data=payload, headers=headers, **kwargs)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
return resp

Expand All @@ -279,17 +263,14 @@ def put(self, data=None, extra_headers: dict = None, **kwargs):
def raw_delete(self, data=None, extra_headers: dict = None, **kwargs):
"""Call Delete and return raw request respond."""
payload = json.dumps(data) if data and "files" not in kwargs else data
headers = (
self._get_headers() | extra_headers
if extra_headers
else self._get_headers()
)
headers = self._get_headers() | extra_headers if extra_headers else self._get_headers()

try:
resp = self._session.delete(self.url(), data=payload, headers=headers, **kwargs)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
return resp

Expand All @@ -315,12 +296,13 @@ class _TimeoutHTTPAdapter(requests.adapters.HTTPAdapter):
and surrounding discussion in that thread for why this is necessary.
Short answer is that Session() objects don't support timeouts.
"""

def __init__(self, timeout=None, *args, **kwargs):
self.timeout = timeout
super(_TimeoutHTTPAdapter, self).__init__(*args, **kwargs)

def send(self, *args, **kwargs):
kwargs['timeout'] = self.timeout
kwargs["timeout"] = self.timeout
return super(_TimeoutHTTPAdapter, self).send(*args, **kwargs)


Expand All @@ -330,6 +312,7 @@ class Api:

It utilizes request sessions to handle retries
"""

token: str | None = None
resource_class: RestResource = RestResource
use_token: bool = True
Expand All @@ -342,9 +325,7 @@ def __init__(self, options: dict):

if "API_PREFIX" not in self.options:
self.options["API_PREFIX"] = API_PREFIX
self.base_url = "{0}/{1}".format(
self.options["DOMAIN"], self.options["API_PREFIX"]
)
self.base_url = "{0}/{1}".format(self.options["DOMAIN"], self.options["API_PREFIX"])
if "TOKEN_TYPE" not in self.options:
self.options["TOKEN_TYPE"] = DEFAULT_TOKEN_TYPE
if "TOKEN_FORMAT" not in self.options:
Expand All @@ -357,8 +338,8 @@ def __init__(self, options: dict):
timeout = self.options.get("SESSION_TIMEOUT", DEFAULT_SESSION_TIMEOUT)
if retries is not None or timeout is not None:
adapter = _TimeoutHTTPAdapter(max_retries=retries, timeout=timeout)
self.session.mount('https://', adapter)
self.session.mount('http://', adapter)
self.session.mount("https://", adapter)
self.session.mount("http://", adapter)

def set_token(self, token):
self.token = token
Expand All @@ -377,7 +358,8 @@ def login(self, password, username=None):
r = self.session.post(url, data=payload, headers=DEFAULT_HEADERS)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
if r.status_code in [200, 201]:
content = json.loads(r.content.decode())
Expand All @@ -388,9 +370,7 @@ def login(self, password, username=None):
self.username = username
return True
else:
logger.error(
"Login failed: " + str(r.status_code) + " " + r.content.decode()
)
logger.error("Login failed: " + str(r.status_code) + " " + r.content.decode())
return False

def logout(self):
Expand All @@ -403,16 +383,15 @@ def logout(self):
r = self.session.post(url, headers=headers)
except requests.exceptions.SSLError as err:
raise HttpCouldNotVerifyServerError(
"Could not verify the server's SSL certificate", err,
"Could not verify the server's SSL certificate",
err,
)
if r.status_code == 204:
logger.info(f"Goodbye @{self.username}")
self.username = None
self.token = None
else:
logger.error(
"Logout failed: " + str(r.status_code) + " " + r.content.decode()
)
logger.error("Logout failed: " + str(r.status_code) + " " + r.content.decode())

def __getattr__(self, item):
"""
Expand Down
Loading