Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,5 @@ cython_debug/
# 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/
*.py~
*~
57 changes: 32 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,47 @@
# apimatic-core-interfaces
[![PyPI][pypi-version]](https://pypi.org/project/apimatic-core-interfaces/)
[![Maintainability Rating][maintainability-badge]][maintainability-url]
[![Vulnerabilities][vulnerabilities-badge]][vulnerabilities-url]
[![PyPI][pypi-version]](https://pypi.org/project/apimatic-core-interfaces/)
[![Maintainability Rating][maintainability-badge]][maintainability-url]
[![Vulnerabilities][vulnerabilities-badge]][vulnerabilities-url]
[![Licence][license-badge]][license-url]

## Introduction
This project contains the abstract layer for APIMatic's core library. The purpose of creating interfaces is to separate out the functionalities needed by APIMatic's core library module. The goal is to support scalability and feature enhancement of the core library and the SDKs along with avoiding any breaking changes by reducing tight coupling between modules through the introduction of interfaces.
This project contains the abstract layer for APIMatic's core library. The purpose of creating interfaces is to separate out the functionalities needed by APIMatic's core library module. The goal is to support scalability and feature enhancement of the core library and the SDKs, while avoiding breaking changes by reducing tight coupling between modules.

## Version supported
Currenty APIMatic supports `Python version 3.7+` hence the apimatic-core-interfaces will need the same versions to be supported.
## Version Supported
Currently, APIMatic supports **Python version 3.7+**, hence the `apimatic-core-interfaces` package requires the same version support.

## Installation
Simply run the command below in your SDK as the apimatic-core-interfaces will be added as a dependency in the SDK.
```python
Run the following command in your SDK (the `apimatic-core-interfaces` package will be added as a dependency):
```bash
pip install apimatic-core-interfaces
```

## Interfaces
| Name | Description |
|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
| [`HttpClient`](apimatic_core_interfaces/client/http_client.py) | To save both Request and Response after the completion of response |
| [`ResponseFactory`](apimatic_core_interfaces/factories/response_factory.py) | To convert the client-adapter response into a custom HTTP response |
| [`Authentication`](apimatic_core_interfaces/types/authentication.py) | To setup methods for the validation and application of the required authentication scheme |
| [`UnionType`](apimatic_core_interfaces/types/union_type.py) | To setup methods for the validation and deserialization of OneOf/AnyOf union types |
| [`Logger`](apimatic_core_interfaces/logger/logger.py) | An interface for the generic logger facade |
| [`ApiLogger`](apimatic_core_interfaces/logger/api_logger.py) | An interface for logging API requests and responses |
| Name | Description |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`HttpClient`](apimatic_core_interfaces/client/http_client.py) | Saves both request and response after the completion of response. |
| [`ResponseFactory`](apimatic_core_interfaces/factories/response_factory.py) | Converts the client-adapter response into a custom HTTP response. |
| [`Authentication`](apimatic_core_interfaces/types/authentication.py) | Sets up methods for the validation and application of the required authentication scheme. |
| [`UnionType`](apimatic_core_interfaces/types/union_type.py) | Sets up methods for the validation and deserialization of OneOf/AnyOf union types. |
| [`Logger`](apimatic_core_interfaces/logger/logger.py) | An interface for the generic logger facade. |
| [`ApiLogger`](apimatic_core_interfaces/logger/api_logger.py) | An interface for logging API requests and responses. |
| [`SignatureVerifier`](apimatic_core_interfaces/security/signature_verifier.py) | Defines the contract for verifying the authenticity of incoming events or webhook requests. |

## Types
| Name | Description |
|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------|
| [`Request`](apimatic_core_interfaces/http/request.py) | Framework-agnostic request model capturing headers, method, path, body, and raw bytes. |
| [`SignatureVerificationResult`](apimatic_core_interfaces/types/signature_verification_result.py) | Provides a structured result of the verification process, including success, failure, and error details. |

## Enumerations
| Name | Description |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------|
| [`HttpMethodEnum`](apimatic_core_interfaces/types/http_method_enum.py ) | Enumeration containig HTTP Methods (GET, POST, PATCH, DELETE) |
| Name | Description |
|----------------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`HttpMethodEnum`](apimatic_core_interfaces/types/http_method_enum.py) | Enumeration containing HTTP methods (GET, POST, PATCH, DELETE).|

[pypi-version]: https://img.shields.io/pypi/v/apimatic-core-interfaces
[license-badge]: https://img.shields.io/badge/licence-MIT-blue
[license-url]: LICENSE
[maintainability-badge]: https://sonarcloud.io/api/project_badges/measure?project=apimatic_core-interfaces-python&metric=sqale_rating
[maintainability-url]: https://sonarcloud.io/summary/new_code?id=apimatic_core-interfaces-python
[vulnerabilities-badge]: https://sonarcloud.io/api/project_badges/measure?project=apimatic_core-interfaces-python&metric=vulnerabilities
[pypi-version]: https://img.shields.io/pypi/v/apimatic-core-interfaces
[license-badge]: https://img.shields.io/badge/licence-MIT-blue
[license-url]: LICENSE
[maintainability-badge]: https://sonarcloud.io/api/project_badges/measure?project=apimatic_core-interfaces-python&metric=sqale_rating
[maintainability-url]: https://sonarcloud.io/summary/new_code?id=apimatic_core-interfaces-python
[vulnerabilities-badge]: https://sonarcloud.io/api/project_badges/measure?project=apimatic_core-interfaces-python&metric=vulnerabilities
[vulnerabilities-url]: https://sonarcloud.io/summary/new_code?id=apimatic_core-interfaces-python
6 changes: 4 additions & 2 deletions apimatic_core_interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@
'client',
'factories',
'types',
'logger'
]
'logger',
'http',
'security'
]
3 changes: 3 additions & 0 deletions apimatic_core_interfaces/http/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__all__ = [
'request',
]
211 changes: 211 additions & 0 deletions apimatic_core_interfaces/http/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# request_adapter.py (Python 3.7+)
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass(frozen=True)
class RequestFile:
"""Framework-agnostic snapshot of an uploaded file (full content)."""
filename: str
content_type: Optional[str]
content: bytes # full file bytes


@dataclass(frozen=True)
class Request:
"""
Compact, framework-agnostic HTTP request snapshot.

Fields:
- method, path, url
- headers: Dict[str, str] (copied; later mutations on the original won't leak)
- raw_body: bytes (wire bytes; frameworks cache safely)
- query/form: Dict[str, List[str]> (multi-value safe)
- cookies: Dict[str, str]
- files: Dict[str, List[RequestFile]> (streams read fully & rewound)
"""
method: str
path: str
url: Optional[str]
headers: Dict[str, str]
raw_body: bytes
query: Dict[str, List[str]] = field(default_factory=dict)
cookies: Dict[str, str] = field(default_factory=dict)
form: Dict[str, List[str]] = field(default_factory=dict)
files: Dict[str, List[RequestFile]] = field(default_factory=dict)

# ---------- helpers ----------

@staticmethod
def _as_listdict(obj: Any) -> Dict[str, List[str]]:
"""MultiDict/QueryDict → Dict[str, List[str]]; Mapping[str,str] → {k:[v]}."""
if not obj:
return {}
getlist = getattr(obj, "getlist", None)
if callable(getlist):
return {k: list(getlist(k)) for k in obj.keys()}
return {k: [obj[k]] for k in obj.keys()}

# ---------- factories (non-destructive) ----------

@staticmethod
async def from_fastapi_request(req, *, max_file_bytes: Optional[int] = None) -> "Request":
"""
Build from fastapi.Request / starlette.requests.Request.

- raw body via await req.body() (Starlette caches; non-destructive)
- parse form only for form/multipart content types
- read UploadFile content **fully** (or up to max_file_bytes if provided) and rewind stream
- copy mappings so later mutations on the original request won't leak
"""
headers = dict(req.headers)
raw = await req.body()
query = Request._as_listdict(req.query_params)
cookies = dict(req.cookies)
url_str = str(req.url)
path = req.url.path

ct = (headers.get("content-type") or headers.get("Content-Type") or "").lower()
parse_form = ct.startswith(("multipart/form-data", "application/x-www-form-urlencoded"))

form: Dict[str, List[str]] = {}
files: Dict[str, List[RequestFile]] = {}

if parse_form:
formdata = await req.form()
# text fields
for k in formdata.keys():
for v in formdata.getlist(k):
if not (hasattr(v, "filename") and hasattr(v, "read")):
form.setdefault(k, []).append(str(v))
# files (rewind after read)
for k in formdata.keys():
for v in formdata.getlist(k):
if hasattr(v, "filename") and hasattr(v, "read"):
fobj = getattr(v, "file", None)
# remember current cursor, read all, then rewind
try:
pos = fobj.tell() if fobj else 0
except Exception:
pos = 0
data = await v.read() # FULL READ
if max_file_bytes is not None and len(data) > max_file_bytes:
data = data[:max_file_bytes]
try:
if fobj:
fobj.seek(pos)
except Exception:
pass
files.setdefault(k, []).append(
RequestFile(v.filename or "", getattr(v, "content_type", None), data)
)

return Request(
method=req.method,
path=path,
url=url_str,
headers=headers,
raw_body=raw,
query=query,
cookies=cookies,
form=form,
files=files,
)

@staticmethod
def from_django_request(req, *, max_file_bytes: Optional[int] = None) -> "Request":
"""
Build from django.http.HttpRequest.

- uses req.body (cached bytes; non-destructive)
- text fields from req.POST; files from req.FILES (read FULL & rewind)
- copies mappings to avoid leaking later mutations
"""
headers = dict(getattr(req, "headers", {}) or {})
url_str = req.build_absolute_uri()
path = req.path
raw = bytes(getattr(req, "body", b"") or b"")
query = Request._as_listdict(getattr(req, "GET", {}))
cookies = dict(getattr(req, "COOKIES", {}) or {})

form = Request._as_listdict(getattr(req, "POST", {}))
files: Dict[str, List[RequestFile]] = {}
files_src = getattr(req, "FILES", None)
if files_src and hasattr(files_src, "getlist"):
for k in files_src.keys():
for f in files_src.getlist(k):
try:
pos = f.tell()
except Exception:
pos = 0
data = f.read() # FULL READ
if max_file_bytes is not None and len(data) > max_file_bytes:
data = data[:max_file_bytes]
try:
f.seek(pos)
except Exception:
pass
files.setdefault(k, []).append(
RequestFile(getattr(f, "name", "") or "", getattr(f, "content_type", None), data)
)

return Request(
method=req.method,
path=path,
url=url_str,
headers=headers,
raw_body=raw,
query=query,
cookies=cookies,
form=form,
files=files,
)

@staticmethod
def from_flask_request(req, *, max_file_bytes: Optional[int] = None) -> "Request":
"""
Build from flask.Request (Werkzeug).

- uses req.get_data(cache=True) (non-destructive)
- text fields from req.form; files from req.files (read FULL & rewind)
- copies mappings to avoid leaking later mutations
"""
headers = dict(req.headers)
url_str = getattr(req, "url", None)
path = req.path
raw = req.get_data(cache=True)
query = Request._as_listdict(req.args)
cookies = dict(req.cookies)

form = Request._as_listdict(req.form)
files: Dict[str, List[RequestFile]] = {}
for k in req.files.keys():
for s in req.files.getlist(k):
stream = getattr(s, "stream", None)
try:
pos = stream.tell() if stream else 0
except Exception:
pos = 0
data = s.read() # FULL READ
if max_file_bytes is not None and len(data) > max_file_bytes:
data = data[:max_file_bytes]
try:
if stream:
stream.seek(pos)
except Exception:
pass
files.setdefault(k, []).append(
RequestFile(s.filename or "", getattr(s, "mimetype", None), data)
)

return Request(
method=req.method,
path=path,
url=url_str,
headers=headers,
raw_body=raw,
query=query,
cookies=cookies,
form=form,
files=files,
)
3 changes: 3 additions & 0 deletions apimatic_core_interfaces/security/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__all__ = [
'signature_verifier'
]
29 changes: 29 additions & 0 deletions apimatic_core_interfaces/security/signature_verifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from abc import ABC, abstractmethod

from apimatic_core_interfaces.http.request import Request
from apimatic_core_interfaces.types.signature_verification_result import SignatureVerificationResult


class SignatureVerifier(ABC):
"""
Abstract base class for signature verification.

Implementations must validate that the provided JSON payload matches
the signature contained in the headers.
"""

@abstractmethod
def verify(self, request: Request) -> SignatureVerificationResult:
"""
Perform signature verification.

Returns:
SignatureVerificationResult: ok=True when the signature is valid; ok=False with the
underlying exception (if any) when invalid or an error occurred.

Notes:
Implementations should NOT raise for runtime verification outcomes; return
VerificationResult.failed(error) instead. Reserve raising for programmer
errors (invalid construction/config).
"""
raise NotImplementedError
3 changes: 2 additions & 1 deletion apimatic_core_interfaces/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
__all__ = [
'http_method_enum',
'authentication',
'union_type'
'union_type',
'signature_verification_result'
]
26 changes: 26 additions & 0 deletions apimatic_core_interfaces/types/signature_verification_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations
from typing import Optional


class SignatureVerificationResult:
"""
Outcome of signature verification.

Attributes:
ok: True if the signature verification passed.
error: Optional exception raised by the verifier. None when ok=True.
"""
ok: bool
error: Optional[Exception] = None

def __init__(self, ok: bool, error: Optional[Exception] = None):
self.ok = ok
self.error = error

@staticmethod
def passed() -> "SignatureVerificationResult":
return SignatureVerificationResult(ok=True, error=None)

@staticmethod
def failed(error: Optional[Exception] = None) -> "SignatureVerificationResult":
return SignatureVerificationResult(ok=False, error=error)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

setup(
name='apimatic-core-interfaces',
version='0.1.6',
version='0.1.7',
description='An abstract layer of the functionalities provided by apimatic-core-library, requests-client-adapter '
'and APIMatic SDKs.',
long_description=long_description,
Expand Down
Loading