# Owslib

This library is designed to help construct Orchard Web Services,
including logging, context, requests and jwt authorization. Inspired with:

* https://github.com/theorchard/python-owslogger/
* https://github.com/theorchard/python-owsrequest/

Navigation:

- [Installation](#installation)
- [Context](#context)
  - [Correlation id](#correlation-id)
  - [Request context](#request-context)
  - [Flask context](#flask-context)
  - [FastAPI or Starlette context](#fastapi-or-starlette-context)
- [Logger](#logger)
    - [Flask request logger](#flask-request-logger)
    - [FastAPI or Starlette request logger](#fastapi-or-starlette-request-logger)
- [Ows client](#ows-client)
    - [Asynchronous Ows client](#asynchronous-ows-client)
    - [Testing Ows client](#testing-ows-client)
    - [Asynchronous Ows client with FastAPI](#asynchronous-ows-client-with-fastapi)
- [Auth](#auth)
    - [Asynchronous JWT Auth](#asynchronous-jwt-auth)
    - [Flask JWT Auth](#flask-jwt-auth)
    - [FastAPI or Starlette JWT Auth](#fastapi-or-starlette-jwt-auth)

## Installation

Using pip:

```shell
pip install -i https://pypi.theorchard.io/pypi/ owslib
```

Using Poetry:

Add orchard repository to `pyproject.toml`:

```toml
[[tool.poetry.source]]
name = "theorchard"
url = "https://pypi.theorchard.io/pypi/"
secondary = true
```

Install using poetry:

```shell
poetry add owslib
```

Owslib also supports `Flask` and `FastAPI` or `Starlette` extension.
Use extras requirements to install specific extension:

```shell
pip install 'owslib[flask,fastapi,starlette]'
```

<div id="context"></div>

## Context

`Owslib` support 2 context variables that can be shared between logging, requests life request cycle
using [PIP-0567- contextvars](https://peps.python.org/pep-0567/) library.

<div id="correlation-id"></div>

### Correlation id

Correlation id is current context unique identifier in UUID4 format.

Usage:

```python
from owslib import context

# Set new correlation id (you can also pass argument value)
correlation_id, token = context.set_correlation_id()

# Get somewhere in the code current correlation id
print(context.get_correlation_id())

# Reset context variable
context.reset_request_context(token)
```

<div id="request-context"></div>

### Request context

Request context stores Orchard request headers, like profile type, profile id, profile uuid, etc.

Usage:

```python
from owslib import context, constants

# Create new request context
request_context = context.RequestContext(
    context_type=constants.CONTEXT_TYPE_PROFILE,
    profile_type="AudienceProfile",
    profile_id=100,
)

# Set new request context
token = context.set_request_context(request_context)

# Reset request context
context.reset_request_context(token)
```

Getting request context from headers:

```python
from owslib import context, constants

# Create new request context
request_context = context.request_context_from_headers(
    headers={
        "Orchard-Profile-Type": "AudienceProfile",
        "Orchard-Profile-Id": "100",
    },
)

assert request_context.context_type == constants.CONTEXT_TYPE_PROFILE
assert request_context.profile_type == "AudienceProfile"
assert request_context.profile_id == 100
```

<div id="flask-context"></div>

### Flask context

Usage:

```python
from flask import Flask

from owslib.ext.flask.context import OwsContext

app = Flask(__name__)

context = OwsContext(add_request_context=True)
context.init_app(app)
```

Correlation id and request context will be passed every request.

<div id="fastapi-or-starlette-context"></div>

### FastAPI or Starlette context

Usage:

```python
from fastapi import FastAPI
from starlette.middleware import Middleware

from owslib.ext.starlette.middleware.context import (
    CorrelationIdMiddleware,
    CorrelationIdHeaderMiddleware,
    RequestContextMiddleware,
)

app = FastAPI(
    middleware=[
        Middleware(CorrelationIdMiddleware),
        Middleware(RequestContextMiddleware),
        Middleware(CorrelationIdHeaderMiddleware),
    ]
)
```

<div id="logger"></div>

## Logger

Ows logger uses DataDog compatible JSON format with output stream handler.

Usage:

```python
from owslib.enums import Environment
from owslib.logger import configure_logging, LogFormat

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "stream": {
            "level": "INFO",
            "class": "owslib.logger.OwsStreamHandler",
        },
        "null": {
            "level": "INFO",
            "class": "logging.NullHandler",
        },
    },
    "loggers": {
        "dmp": {
            "handlers": ["stream"],
            "level": "INFO",
        },
        "owslib.ext.starlette": {
            "handlers": ["stream"],
            "level": "INFO",
        },
        "sqlalchemy.engine": {
            "handlers": ["stream"],
            "level": "WARNING",
        },
        "uvicorn": {
            "handlers": ["stream"],
            "level": "INFO",
        },
        "uvicorn.access": {
            "handlers": ["null"],
            "level": "INFO",
        },
    },
}

configure_logging(
    LOGGING_CONFIG,
    environment=Environment.DEV,
    service_name="ows-permissions",
    service_version="1.0.0",
    log_format=LogFormat.JSON,
    logger_name="ows1",
    debug=False,  # Use debug=True to set all handlers level to DEBUG
)
```

Ows logger support different log formats:

* `LogFormat.JSON` - DataDog compatible JSON format
* `LogFormat.JSON_PRETTY` - DataDog compatible JSON format with ident (useful for local development)
* `LogFormat.DEBUG` - Colored stream formatter (local dev only)
* `LogFormat.DEBUG_EXTRA` - Colored stream formatter with log extra parameters output as a table (local dev only)

<div id="flask-request-logger"></div>

### Flask request logger

Usage:

```python
from flask import Flask

from owslib.ext.flask.logger import RequestLogger

app = Flask(__name__)

request_logger = RequestLogger(exclude_paths=["/hello/", "/openapi.json"])
request_logger.init_app(app)
```

This will output all http request.

<div id="fastapi-or-starlette-request-logger"></div>

### FastAPI or Starlette request logger

Usage:

```python
from fastapi import FastAPI
from starlette.middleware import Middleware

from owslib.ext.starlette.middleware.logger import RequestLoggerMiddleware

app = FastAPI(middleware=[
    Middleware(RequestLoggerMiddleware, exclude_paths=["/hello/", "/openapi.json"])
])
```

<div id="ows-client"></div>

## Ows client

Ows client uses [httpx](https://www.python-httpx.org/) which is a fully featured HTTP client for Python 3,
provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.

Usage:

```python
from owslib.client import OwsClient
from owslib.enums import Environment

ows_client = OwsClient(environment=Environment.QA, service_name="ows-dmp")

# Making synchronous request
response = ows_client.get("ows-permissions", path="/hello/")
```

Using in dev and test environments:

```python
import os

from owslib.client import OwsClient
from owslib.enums import Environment

os.environ["OWSREQUEST_SERVICE_MAP"] = "{\"ows-permissions\": \"http://localhost:5005/\"}"

ows_client = OwsClient(environment=Environment.DEV, service_name="ows-dmp")

# Making synchronous request
response = ows_client.get("ows-permissions", path="/hello/")
```

<div id="asynchronous-ows-client"></div>

### Asynchronous Ows client

Usage:

```python
from owslib.client import AsyncOwsClient
from owslib.enums import Environment

ows_client = AsyncOwsClient(environment=Environment.QA, service_name="ows-dmp")

# Making synchronous request
response = await ows_client.get("ows-permissions", path="/hello/")

# Close transport and proxies.
await ows_client.close()
```

<div id="testing-ows-client"></div>

### Testing Ows client

Ows client using [respx](https://lundberg.github.io/respx/) for mocking out the HTTPX, and HTTP Core, libraries.

Pytest usage:

```python
import httpx

from owslib.client import OwsClient
from owslib.client.mock import OwsClientMock
from owslib.enums import Environment

ows_client = OwsClient(environment=Environment.TEST, service_name="ows-test")


def test_ows_permissions_hello(ows_client_mock: OwsClientMock) -> None:
    response_status_code = 200
    response_json = {"status": "ok"}

    ows_client_mock.get("ows-permissions", path="/hello/").mock(
        return_value=httpx.Response(status_code=response_status_code, json=response_json)
    )

    response = ows_client.get("ows-permissions", path="/hello/")

    assert response.status_code == response_status_code
    assert response.json() == response_json
```

This will also work for asynchronous Ows client:

```python
import httpx
import pytest

from owslib.client import AsyncOwsClient
from owslib.client.mock import OwsClientMock
from owslib.enums import Environment

ows_client = AsyncOwsClient(environment=Environment.TEST, service_name="ows-test")


@pytest.mark.asyncio
async def test_ows_permissions_hello(ows_client_mock: OwsClientMock) -> None:
    response_status_code = 200
    response_json = {"status": "ok"}

    ows_client_mock.get("ows-permissions", path="/hello/").mock(
        return_value=httpx.Response(status_code=response_status_code, json=response_json)
    )

    response = await ows_client.get("ows-permissions", path="/hello/")

    assert response.status_code == response_status_code
    assert response.json() == response_json
```

<div id="asynchronous-ows-client-with-fastapi"></div>

#### Asynchronous Ows client with FastAPI

You can use asynchronous Ows client more efficiently with FastAPI asynchronous endpoint aggregating results from the given coroutines/futures.

Usage:

```python
import asyncio
from typing import Any

from fastapi import FastAPI

from owslib.client import AsyncOwsClient
from owslib.enums import Environment

ows_client = AsyncOwsClient(environment=Environment.QA, service_name="ows-dmp")

app = FastAPI(on_shutdown=[ows_client.close])


@app.get("/hello/")
async def hello() -> Any:
    users_response, permissions_response = await asyncio.gather(
        ows_client.get("ows-users", path="/hello/"),
        ows_client.get("ows-permissions", path="/hello/")
    )
    return {
        "users": users_response.json(),
        "permissions": users_response.json(),
    }
```

<div id="auth"></div>

## Auth

Owslib uses [JOSE](https://docs.authlib.org/en/latest/jose/index.html) implementation (JWT, JWK, JWS, JWE, JWA) for authenticating requests.

Usage:

```python
from owslib.auth import JWTAuth

auth = JWTAuth(
    jwks_url="https://qalogin.theorchard.com/.well-known/jwks.json",
    claims_options={
        "aud": {
            "essential": True,
            "values": ["aud1", "aud2"],
        },
        "iss": {"essential": True},
        "sub": {"essential": True},
    }
)

try:
    token_claims = auth.authenticate(authorization="bearer token")
except Exception as exc:
    print(exc)
```

or using default environment JWT auth factory:

```python
from owslib.auth import jwt_auth_from_config
from owslib.enums import Environment

auth = jwt_auth_from_config(environment=Environment.QA)

try:
    token_claims = auth.authenticate(authorization="bearer token")
except Exception as exc:
    print(exc)
```

<div id="asynchronous-jwt-auth"></div>

### Asynchronous JWT Auth

JWT auth also supports asynchronous implementation.

Usage:

```python
from owslib.auth import async_jwt_auth_from_config
from owslib.enums import Environment

auth = async_jwt_auth_from_config(environment=Environment.PROD)

try:
    token_claims = await auth.authenticate(authorization="bearer token")
except Exception as exc:
    print(exc)
```

<div id="flask-jwt-auth"></div>

### Flask JWT Auth

Usage:

```python
from flask import Flask

from owslib.enums import Environment
from owslib.ext.flask.auth import JWTAuth

app = Flask(__name__)

jwt_auth = JWTAuth(
    environment=Environment.PROD,
    exclude_paths=["/public/"],
    enabled=True,  # Force enable/disable
)
jwt_auth.init_app(app)
```

<div id="fastapi-or-starlette-jwt-auth"></div>

### FastAPI or Starlette JWT Auth

Usage:

```python
from fastapi import FastAPI
from starlette.middleware import Middleware

from owslib.enums import Environment
from owslib.ext.starlette.middleware.auth import JWTAuthenticationMiddleware

app = FastAPI(middleware=[
    Middleware(
        JWTAuthenticationMiddleware,
        environment=Environment.PROD,
        exclude_paths=["/public/"],
        enabled=True,  # Force enable/disable
    )
])
```
