Metadata-Version: 2.5
Name: odatapy
Version: 5.0.1
Summary: OData v4.0 / v4.01 server framework and client for Python, modelled on Apache Olingo
Project-URL: Homepage, https://github.com/Beroe-Inc/Python-OData-5.0.0
Project-URL: Source, https://github.com/Beroe-Inc/Python-OData-5.0.0
Project-URL: Issues, https://github.com/Beroe-Inc/Python-OData-5.0.0/issues
Project-URL: Changelog, https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/CHANGELOG.md
Project-URL: Olingo comparison, https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/OLINGO_COMPARISON.md
Author-email: "Beroe Inc." <amit.goel@beroe-inc.com>
Maintainer-email: Amit Goel <amit.goel@beroe-inc.com>
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: asgi,csdl,edm,odata,odata-v4,odata4,olingo,rest,wsgi
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: client
Requires-Dist: httpx>=0.27; extra == 'client'
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: jsonschema>=4; extra == 'dev'
Requires-Dist: lxml>=5; extra == 'dev'
Requires-Dist: mypy==2.3.1; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff==0.15.15; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# odatapy — OData v4 for Python

An OData **v4.0 / v4.01** server framework and client for Python, modelled on
[Apache Olingo OData 4](https://olingo.apache.org/) (Java). It is
transport-neutral: the server mounts into any ASGI or WSGI application, and the
client works over httpx or in-process.

> **Status: stable.** Feature-complete against the OData v4.01 specifications
> for both server and client. 2,001 tests pass on CPython 3.10 through 3.14,
> ten of them over a real TCP socket; `ruff` and `mypy` are clean; and the suite
> passes against the built distribution with the source tree removed. The client
> and the metadata serializers are verified against the public OData reference
> services, and the metadata documents validate against the OASIS CSDL XML and
> JSON schemas. [Coverage compared with Apache Olingo](#coverage-compared-with-apache-olingo)
> lists what is covered, what is deliberately not ported, and the remaining
> limitations. The version number follows the Olingo release the design is
> modelled on.

## Installation

```bash
pip install odatapy              # server + in-process client (no dependencies)
pip install "odatapy[client]"    # adds httpx for the networked client
```

Python 3.10 or newer. No required runtime dependencies.

## Documentation

[`docs/index.md`](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/index.md)
is the map. Every code sample on every page was executed before it was written
down.

| Page | What it covers |
|---|---|
| [Tutorial](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/tutorial.md) | Build a working service from nothing, in runnable steps |
| [Server guide](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/server.md) | The pipeline, the processor protocols, and a worked SQLite-backed service |
| [Client guide](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/client.md) | Synchronous and asyncio use, CRUD, batch, paging, auth |
| [Modelling](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/edm.md) | The CSDL provider, types, operations, annotations |
| [Query options](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/query-options.md) | Every option with its real payload, and what is not supported |
| [Deployment](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/deployment.md) | gunicorn, uvicorn, Flask, FastAPI, proxies, paging guards |
| [Migrating from Olingo](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/migrating-from-olingo.md) | Concept-by-concept translation for Java developers |
| [Olingo comparison](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/OLINGO_COMPARISON.md) | What is covered, what is not ported, known limitations |
| [Interoperability](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/INTEROP.md) | Results against the public OData reference services |

## Features

- **EDM type system** — every OData primitive type with its literal grammar,
  facet validation and URI-literal encoding; enums with flags, type definitions,
  derived types, and the 4.01 abstract and path types. The runtime model is
  built from a declarative CSDL provider or loaded from a `$metadata` document
  in either format.
- **Server framework** — a transport-neutral pipeline of parse, validate,
  dispatch and process, with consolidated processor protocols, extension hooks
  for content types, ETags and batch transactions, and a reusable in-memory
  reference service.
- **URI & query options** — the full resource-path grammar including
  key-as-segment, type casts, `$crossjoin`, `$all`, `$entity` and composable
  functions, plus `$filter` and `$orderby` with every operator and function
  evaluated, `$select`, `$expand` with nested options and `$levels`, `$compute`,
  `$search` and `$apply` parsed to trees and evaluated, and the full validator
  matrix.
- **Serialization** — JSON at all three metadata levels with derived types,
  exact `Decimal` digits, `IEEE754Compatible` and operation advertisement;
  first-class Atom and XML with inline expansion; `$metadata` as CSDL XML and
  CSDL JSON, both round-tripping byte-identically; delta payloads in the 4.0 and
  4.01 shapes.
- **Protocol features** — media entities, `$batch` with atomic changesets and
  content-ID references in both the multipart and JSON formats, bound and
  unbound operations, deep insert, `$ref`, singletons, ETags and preconditions,
  every `Prefer` option with an accurate `Preference-Applied`, server-driven
  paging with stable tokens, change tracking, and `respond-async` with polling
  and cancellation.
- **Geospatial** — the Geography and Geometry families with WKT and GeoJSON.
- **HTTP adapters** — ASGI (FastAPI, Starlette) and WSGI (Flask, Django,
  gunicorn) honouring mount prefixes, chunked bodies, proxy headers, streaming
  responses and method tunnelling on POST.
- **Client** — synchronous and asyncio clients sharing one implementation, a
  fluent URI builder, filter, search and order-by builders, full CRUD, media and
  reference operations, a `$batch` builder, paging that follows `nextLink`,
  authentication and retries, cached `$metadata`, and a zero-network in-process
  transport for tests.
- **Extensions** — `odatapy.ext.sql` translates a `$filter` tree to a
  parameterised `WHERE` clause in four SQL dialects, and
  `odatapy.ext.sqlalchemy` produces SQLAlchemy Core expressions.

## Server in 30 lines

```python
from decimal import Decimal
from odatapy.data import Entity, ValueType
from odatapy.edm.csdl import *
from odatapy.server import OData
from odatapy.server.adapters import ODataWSGIApp
from odatapy.server.inmemory import InMemoryDataStore, InMemoryProcessor

class Provider(CsdlAbstractEdmProvider):
    def get_schemas(self):
        product = CsdlEntityType(
            name="Product", key=[CsdlPropertyRef(name="ID")],
            properties=[
                CsdlProperty(name="ID", type="Edm.Int32", nullable=False),
                CsdlProperty(name="Name", type="Edm.String"),
                CsdlProperty(name="Price", type="Edm.Decimal", precision=10, scale=2),
            ],
        )
        container = CsdlEntityContainer(
            name="Container",
            entity_sets=[CsdlEntitySet(name="Products", type="Demo.Product")],
        )
        return [CsdlSchema(namespace="Demo", entity_types=[product], entity_container=container)]

odata = OData.new_instance()
handler = odata.create_handler(odata.create_service_metadata(Provider()))
store = InMemoryDataStore()
p = Entity(); p.set_property("ID", 1, ValueType.PRIMITIVE, "Edm.Int32")
p.set_property("Name", "Apple", ValueType.PRIMITIVE, "Edm.String")
p.set_property("Price", Decimal("1.50"), ValueType.PRIMITIVE, "Edm.Decimal")
store.set_entities("Products", [p])
handler.register(InMemoryProcessor(store))

app = ODataWSGIApp(handler)   # a WSGI app: mount in Flask/gunicorn/wsgiref
```

A complete runnable service (WSGI + ASGI) is in
[`examples/demo_service.py`](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/examples/demo_service.py):

```bash
python examples/demo_service.py
curl http://localhost:8080/\$metadata
curl "http://localhost:8080/Products?\$filter=Price gt 2&\$orderby=Price desc"
```

For a real back end, implement the processor protocols in `odatapy.server.processor`
(entity collection, entity, primitive/complex property, media, reference,
function/action, batch) and register your implementation instead of
`InMemoryProcessor`. The in-memory processor is a reference implementation and a
test oracle, not a production data layer.

## Client

```python
from odatapy.client import ODataClient
from odatapy.client.transport import HttpxTransport
from odatapy.client.filters import Field, and_

client = ODataClient("http://host/svc", HttpxTransport())

uri = (client.entity_set_uri("Products")
       .filter(and_(Field("Price").gt(10), Field("Name").startswith("A")))
       .select("Name", "Price").top(10))
for entity in client.get_entity_set(uri).entities:
    print(entity.get_value("Name"))

client.create_entity("Products", {"ID": 42, "Name": "Kiwi", "Price": 3.0})
client.update_entity(client.entity_uri("Products", 42), {"Price": 3.5})   # PATCH
client.update_property(client.entity_uri("Products", 42).append_property_segment("Name"), "Kiwi Gold")
print(client.get_count(client.entity_set_uri("Products")))
```

`AsyncODataClient` is the asyncio twin - every method is a coroutine and the
request building and payload parsing are the same code, so results are
identical:

```python
from odatapy.client import AsyncODataClient, AsyncHttpxTransport

async with AsyncHttpxTransport() as transport:
    client = AsyncODataClient("http://host/svc", transport)
    async for entity in client.iter_entities(client.entity_set_uri("Products")):
        print(entity.get_value("Name"))
```

`HandlerTransport` (and `AsyncHandlerTransport`) routes requests to an
in-process server handler with no network, as the test suite does throughout.
Transports add redirects, timeouts, a `RetryPolicy` honouring `Retry-After`,
proxies and authentication (`BasicAuth`, `BearerAuth`,
`OAuth2ClientCredentialsAuth`). `EdmEnabledODataClient` caches `$metadata`
(revalidating with `If-None-Match`) so payloads come back as `Decimal`,
`datetime`, `UUID`, `bytes` and geo values. `$batch` is built with
`client.batch()` in either the multipart or the OData 4.01 JSON format.

## Custom back ends: the filter visitor

`$filter` parses to an expression tree; implement an `ExpressionVisitor` to
evaluate or translate it. `odatapy.ext.sql` is a worked example:

```python
from odatapy.ext.sql import filter_to_sql
where, params = filter_to_sql(uri_info.filter.expression, column_map={"Name": "product_name"})
# where == "(product_name LIKE ? ESCAPE '\')", params == ["A%"]  (for startswith(Name,'A'))
```

## Coverage compared with Apache Olingo

odatapy is about 31,800 lines of Python against roughly 100,000 lines of Java in
Olingo's `commons`, `server` and `client` modules. The ratio is not a coverage
ratio: Olingo splits every concept into an interface module and an
implementation module and ships two HTTP bindings plus a reference service.

[`docs/OLINGO_COMPARISON.md`](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/OLINGO_COMPARISON.md)
is the detailed comparison. Every feature listed there was verified through a
real request rather than only parsed. The summary:

| Area | Status |
|---|---|
| Primitive types, literals, facets | Complete, including 4.01 abstract and path types, sub-microsecond precision, enum flags and type definitions |
| `$metadata` XML and JSON CSDL | Complete in both formats, and both round-trip byte-identically through the parsers |
| Vocabulary annotations | Constant and dynamic expressions, in both formats, parsed and emitted |
| Resource paths | Complete, including key-as-segment, type casts, `$crossjoin`, `$all`, `$entity`, lambdas and composable functions |
| `$filter` and `$orderby` | Every operator and function, evaluated; bound to the model, so unknown properties are 400; strict three-valued null logic |
| `$search` and `$apply` | Full grammars parsed to trees and evaluated, including nested `groupby`, `compute`, `from` and the top/bottom family |
| URI validator | The full method by resource-kind by option matrix, with `Allow` |
| JSON serialization | All metadata levels, derived types, exact `Decimal`, `IEEE754Compatible`, delta in both versions, operation advertisement |
| Atom and XML payloads | First-class: inline expansion, media entries, references, delta, correct namespaces |
| Deserialization | Type and facet validation, derived-type casts, open types, strict mode, action parameters |
| Server pipeline and headers | Version negotiation, correct status codes, all `Prefer` options, ETags, `HEAD`, `OPTIONS`, debug support |
| `$batch` | Byte-level parsing, binary-safe, atomic changesets, content-ID references, and the 4.01 JSON format |
| Adapters | WSGI and ASGI with mount prefixes, chunked bodies, proxy headers and streaming |
| Client | Synchronous and asynchronous, full CRUD, batch, paging, authentication and retries |
| Extensions | `$filter` to SQL in four dialects, and a SQLAlchemy expression bridge |
| Deliberately not ported | Servlet and Netty bindings, NTLM and Kerberos authentication, the reflection-based client builder, `server-core-ext`, `tecsvc` |

Known limitations are listed in section 4 of that document. The main ones: the
bundled in-memory processor is a reference implementation rather than a data
layer, `odata.callback` push notification is unsupported, custom `$apply`
aggregation methods return 501, and the geospatial predicates are planar
approximations rather than geodesics.

## Package layout

```
odatapy/
├── edm/         primitives, geo, names, csdl (provider), model (runtime Edm),
│                annotations (vocabulary expressions), xml_io and json_csdl
│                (metadata parse + providers), json_value
├── data/        Entity, Property, ComplexValue, EntityCollection, Link,
│                ContextURL, Delta, Operation, Parameter
├── http/        ContentType/Accept, HttpMethod, HttpStatusCode, Prefer, headers
├── errors.py    ODataError + exception hierarchy
├── server/      request/response, handler, dispatcher, processor, negotiator,
│                support (extension hooks), default_processor,
│                uri/ (parser, expression tree and parser, search_parser,
│                apply_parser, query options, resource, validator),
│                serializer/ (json, xml, metadata, factory),
│                deserializer/ (json, xml),
│                batch, apply, eval, inmemory, adapters/ (asgi, wsgi, common)
├── client/      client (sync + async), uri_builder, filters, serialization,
│                transport, auth, batch, responses
└── ext/         sql (filter → SQL WHERE), sqlalchemy (→ Core expressions)
```

## Development

```bash
pip install -e ".[dev]"
pytest -q
ruff check src tests examples
python -m build && twine check dist/*
```

Releases are published from GitHub Actions with PyPI Trusted Publishing
(see `.github/workflows/publish.yml`); no API tokens are stored.

## License

Apache License 2.0. See `LICENSE` and `NOTICE`. Apache Olingo is a trademark of
The Apache Software Foundation; this project is an independent port and is not
endorsed by the ASF.
