Skip to content

Commit

Permalink
Merge pull request #37 from privacera/paig_common
Browse files Browse the repository at this point in the history
paig-common added #34
  • Loading branch information
pravin-bansod authored Sep 5, 2024
2 parents 464a41a + 69375a7 commit 5c86092
Show file tree
Hide file tree
Showing 31 changed files with 2,193 additions and 0 deletions.
64 changes: 64 additions & 0 deletions .github/workflows/paig-common-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: paig-common-ci

on:
push:
branches: [ "main" ]
paths:
- 'paig-common/**'
pull_request:
branches: [ "main" ]
paths:
- 'paig-common/**'

permissions:
contents: read

jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Ensure the repo is checked out

- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: |
python3 -m pip install virtualenv
virtualenv -p python3 venv && . venv/bin/activate
pip install twine build pytest pytest-cov
pip install -r paig-common/requirements.txt
- name: Test with pytest
run: |
filepath=$PWD
. venv/bin/activate && cd paig-common/src
python3 -m pytest --cov="." --cov-report term --cov-report xml:$filepath/coverage.xml --junitxml=$filepath/junit.xml tests
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml

- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: junit-results
path: junit.xml

- name: Build the wheel
run: |
. venv/bin/activate
cd paig-common
python3 -m build -w
cd ..
- name: Install created package
run: |
rm -rf venv
virtualenv -p python3 venv && . venv/bin/activate
cd paig-common
pip install dist/*.whl
65 changes: 65 additions & 0 deletions .github/workflows/paig-common-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: paig-common upload to PyPI

on:
workflow_dispatch: # Allows manual trigger

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # Ensure the repo is checked out

- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: |
python3 -m pip install virtualenv
virtualenv -p python3 venv && . venv/bin/activate
pip install twine build pytest pytest-cov
pip install -r paig-common/requirements.txt
- name: Test with pytest
run: |
filepath=$PWD
. venv/bin/activate && cd paig-common/src
python3 -m pytest --cov="." --cov-report term --cov-report xml:$filepath/coverage.xml --junitxml=$filepath/junit.xml tests
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml

- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: junit-results
path: junit.xml

- name: Build the wheel
run: |
. venv/bin/activate
cd paig-common
python3 -m build -w
- name: Install created package
run: |
rm -rf venv
virtualenv -p python3 venv && . venv/bin/activate
cd paig-common
pip install dist/*.whl
- name: Release to PyPI
env:
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PYPI_API_TOKEN }}
run: |
. venv/bin/activate
pip install twine
twine upload paig-common/dist/*
32 changes: 32 additions & 0 deletions paig-common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Common library for Privacera AI Governance Applications
The paig_common provides the common classes/functions used by the privacera AI governance applications.


## Development
```
# go inside the project repository
cd paig-common
# setup environment
python3 -m venv venv
# activate environment
. venv/bin/activate
# install dependencies
pip install -r requirements.txt
```

## Tests
```
# go to shield directory inside the project repository
cd paig-common
# activate environment
. venv/bin/activate
# run tests with coverage
cd src
sh run_unit_test.sh
```
31 changes: 31 additions & 0 deletions paig-common/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "paig-common"
dynamic = ["version"]
authors = [
{ name = "Privacera ", email = "[email protected]" },
]
description = "PAIG Shield Common Library"
readme = "README.md"
license = {file = "../LICENSE"}
requires-python = ">=3.8.1"
classifiers = [
"Programming Language :: Python :: 3",
"License :: Other/Proprietary License",
"Operating System :: OS Independent",
]
dependencies = [
"urllib3>=2.0.6",
"cryptography>=41.0.4",
"jproperties>=2.1.1"
]

[project.urls]
"Homepage" = "https://github.com/pypa/paig_common"
"Bug Tracker" = "https://github.com/pypa/paig_common/issues"

[tool.hatch.version]
path = "src/paig_common/VERSION"
15 changes: 15 additions & 0 deletions paig-common/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
urllib3==2.2.1
cryptography==42.0.5
build==1.2.1
pytest==8.0.0
pytest-cov==4.1.0
pytest-html==4.1.1
pytest-mock==3.12.0
twine==5.0.0
boto3==1.34.82
python-gitlab==4.4.0
jproperties==2.1.1
Flask==3.0.3
fastapi==0.111.0
httpx==0.27.0
pytest-asyncio==0.23.7
1 change: 1 addition & 0 deletions paig-common/src/paig_common/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '0.0.1'
Empty file.
141 changes: 141 additions & 0 deletions paig-common/src/paig_common/async_base_rest_http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import asyncio
import json
from typing import Collection
import httpx

from . import config_utils
from .async_http_transport import AsyncHttpTransport
import logging

logger = logging.getLogger(__name__)


class AsyncBaseRESTHttpClient:
"""
An asynchronous base class for making HTTP requests to a RESTful API using httpx.
"""
_setup_done = False # Flag to track if setup has been performed
_setup_lock = asyncio.Lock()
_max_retries = config_utils.get_property_value_int("http.rest.client.max_retries", 4)
_backoff_factor = config_utils.get_property_value_int("http.rest.client.backoff_factor", 1)
_allowed_methods_str = config_utils.get_property_value("http.rest.client.allowed_methods",
'["GET", "POST", "PUT", "DELETE"]')
_allowed_methods: Collection[str] = eval(_allowed_methods_str)
_status_forcelist_str = config_utils.get_property_value("http.rest.client.status_forcelist",
'[500, 502, 503, 504]')
_status_forcelist: Collection[int] = eval(_status_forcelist_str)

def __init__(self, base_url):
"""
Initialize the AsyncBaseRESTHttpClient instance.
Args:
base_url (str): The base URL of the REST API.
"""
self.baseUrl = base_url

@staticmethod
async def setup():
"""
Set up the Async HTTP transport configuration (called only once).
Returns:
None
"""
async with AsyncBaseRESTHttpClient._setup_lock:
if not AsyncBaseRESTHttpClient._setup_done:
connect_timeout_sec = config_utils.get_property_value_float("http.rest.client.connect_timeout_sec", 2.0)
read_timeout_sec = config_utils.get_property_value_float("http.rest.client.read_timeout_sec", 7.0)
await AsyncHttpTransport.setup(
connect_timeout_sec=connect_timeout_sec,
read_timeout_sec=read_timeout_sec
)
AsyncBaseRESTHttpClient._setup_done = True

def get_auth(self):
"""
Get the authentication configuration for the HTTP requests.
Returns:
None or httpx.Auth: None if no authentication is needed, an httpx.Auth object otherwise.
"""
return None

def get_default_headers(self):
"""
Get the default HTTP headers to include in each request.
Returns:
dict: A dictionary containing default headers.
"""
return {"Content-Type": "application/json", "Accept": "application/json"}

async def request(self, *args, **kwargs):
"""
Make an asynchronous HTTP request to the API.
Args:
*args: Positional arguments for the request method.
**kwargs: Keyword arguments for the request.
Returns:
AsyncReturnValue: The HTTP response object.
"""
headers = kwargs.get("headers", {})

if "url" in kwargs:
kwargs["url"] = self.baseUrl + kwargs["url"]

updated_headers = self.get_default_headers()
updated_headers.update(headers)
kwargs["headers"] = updated_headers

auth = self.get_auth()
if auth is not None:
kwargs["auth"] = auth

await AsyncBaseRESTHttpClient.setup()
client = await AsyncHttpTransport.get_client()
retries = AsyncBaseRESTHttpClient._max_retries
backoff_factor = AsyncBaseRESTHttpClient._backoff_factor

for attempt in range(retries + 1):
try:
response = await client.request(*args, auth=auth, **kwargs)
if response.status_code not in AsyncBaseRESTHttpClient._status_forcelist:
return AsyncReturnValue(response)
except httpx.RequestError as exc:
if attempt == retries:
# Log error and raise exception after max retries
logger.error(f"Max retries reached without a successful response getting this exception: {exc}")
raise httpx.RequestError(
f"Max retries reached without a successful response getting this exception: {exc}")
# Wait before retrying
await asyncio.sleep(backoff_factor * (2 ** attempt))

async def get(self, *args, **kwargs):
return await self.request(method='GET', *args, **kwargs)

async def post(self, *args, **kwargs):
return await self.request(method='POST', *args, **kwargs)

async def put(self, *args, **kwargs):
return await self.request(method='PUT', *args, **kwargs)

async def delete(self, *args, **kwargs):
return await self.request(method='DELETE', *args, **kwargs)

async def patch(self, *args, **kwargs):
return await self.request(method='PATCH', *args, **kwargs)


class AsyncReturnValue:
def __init__(self, response: httpx.Response):
self.status_code = response.status_code
self.text = response.text

def json(self):
return json.loads(self.text)

def __str__(self):
return f"Response return value: (status_code={self.status_code}, text={self.text})"
Loading

0 comments on commit 5c86092

Please sign in to comment.