Metadata-Version: 2.5
Name: odatapy
Version: 5.0.0a2
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: mypy; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
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: alpha.** The library is usable for JSON-first OData services and
> clients, and ships with 190+ tests. It is **not** at feature parity with
> Olingo: the URI parser, serializers and in-memory processor cover the common
> OData surface but skip or simplify a number of spec areas. Read
> [Coverage compared with Apache Olingo](#coverage-compared-with-apache-olingo)
> before relying on it in production. The version number (5.0.0aN) follows the
> Olingo release the design is modelled on, not a parity claim.

## 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.

## Features

- **EDM type system** — all OData primitive types with literal grammars, facet
  validation and URI-literal encoding; runtime EDM built from a declarative CSDL
  provider or loaded from a `$metadata` XML document.
- **Server framework** — transport-neutral request pipeline (parse → validate →
  dispatch → processor), consolidated processor protocols, and a reusable
  in-memory reference service.
- **URI & query options** — resource paths plus `$filter` (expression tree +
  visitor), `$select`, `$expand` (nested options, `$levels`), `$orderby`, `$top`,
  `$skip`, `$count`, `$search`, `$format`, and `$apply` (`filter`/`aggregate`/
  `groupby` evaluated in memory).
- **Serialization** — JSON (metadata levels none/minimal/full) and Atom/XML
  payloads; `$metadata` as XML (EDMX/CSDL) and JSON CSDL; delta responses.
- **Protocol features** — media entities (`$value`, `HasStream`), `$batch` with
  changesets and `$<id>` references, bound and unbound functions/actions, deep
  insert with `@odata.bind`, `$ref`, singletons, ETags with `If-Match` /
  `If-None-Match`, server-driven paging, and `Prefer: respond-async`.
- **Geospatial** — Geography*/Geometry* types with WKT and GeoJSON.
- **HTTP adapters** — ASGI (FastAPI/Starlette) and WSGI (Flask/Django/gunicorn)
  with `X-HTTP-Method` tunnelling on POST.
- **Client** — fluent `URIBuilder`, `$filter` builder, CRUD, property updates,
  functions/actions, `$count`, service document, `$metadata` → EDM with caching,
  JSON and XML response reading, httpx transport, and a zero-network in-process
  transport for tests.
- **Extensions** — `odatapy.ext.sql` translates a `$filter` tree to a
  parameterised SQL `WHERE` clause (a worked `ExpressionVisitor`).

## 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")))
```

`HandlerTransport` routes requests to an in-process server handler with no
network (used throughout the test suite). `AsyncHttpxTransport` provides an
awaitable transport; the high-level `ODataClient` itself is synchronous today.

## 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 ?)", params == ["A%"]  (for startswith(Name,'A'))
```

## Coverage compared with Apache Olingo

odatapy is about 10,000 lines of Python against roughly 100,000 lines of Java in
Olingo's `commons`, `server` and `client` modules. A detailed, file-referenced
comparison with reproduction snippets is in
[`docs/OLINGO_COMPARISON.md`](https://github.com/Beroe-Inc/Python-OData-5.0.0/blob/main/docs/OLINGO_COMPARISON.md).
The short version:

| Area | Coverage | Main gaps |
|---|---|---|
| Primitive types, literals, facets | High | DateTimeOffset/TimeOfDay precision not enforced; 4.01 abstract types (`Edm.Untyped`, `Edm.PrimitiveType`) missing |
| CSDL provider + `$metadata` XML | Medium-high | `edmx:Reference`, `Annotations` targets, `Term`, `Scale="variable"`, container `Extends` not modelled; version fixed at 4.0 |
| `$metadata` JSON CSDL | Medium | Functions/actions/imports/type definitions and `$IsFlags` not emitted; property annotations misplaced |
| Resource-path parser | Medium | No key-as-segment, type-cast segments, `$crossjoin`/`$all`, lambda in paths; key predicates not validated against the EDM |
| `$filter` expression parser | Medium | No `in`, `any`/`all`, `$it`/`$root`, `hassubset`; no EDM type binding, so unknown properties are not rejected as 400 |
| `$search` / `$apply` | Low | Stored as raw strings (no AST); `$apply` evaluation limited to `filter`/`aggregate`/`groupby` |
| URI validator | Low-medium | Only a subset of Olingo's method × resource × option matrix |
| JSON serializer | Medium | Derived types and `@odata.type` payload casts ignored; no IEEE754Compatible; incomplete full-metadata annotations; delta format mixes 4.0 and 4.01 |
| JSON deserializer | Medium | No type/facet validation of incoming values, no strict mode, no action-parameter deserializer |
| Atom/XML payloads | Low-medium | No navigation links or inline expand, service document namespace wrong, error details dropped |
| Server pipeline & headers | Medium | No `OData-Version`/`OData-MaxVersion` validation, 406/415 mapped to 400, `Preference-Applied` only for `return=minimal` |
| `$batch` | Medium | Changesets are not atomic, `$<id>` rewrite is a blind byte replace, binary parts are decoded as UTF-8 |
| In-memory processor | Medium | Navigation paths in reads, property writes, duplicate-key detection and nested `$expand` options are incomplete |
| Client | Low-medium | No batch, streaming/paging iterator, `$ref` helpers, or async client; filter builder covers comparison/logical/string ops only |
| Not ported | — | DebugSupport, CustomETagSupport, ServiceMetadataETagSupport, CustomContentTypeSupport, Olingo `server-core-ext`, NTLM auth |

## Package layout

```
odatapy/
├── edm/         primitives, geo, names, csdl (provider), model (runtime Edm),
│                xml_io (metadata parse + XmlEdmProvider), json_value
├── data/        Entity, Property, ComplexValue, EntityCollection, Link, ContextURL, Delta
├── http/        ContentType/Accept, HttpMethod, HttpStatusCode, Prefer, headers
├── errors.py    ODataError + exception hierarchy
├── server/      request/response, handler, dispatcher, processors, negotiator,
│                uri/ (parser, expression tree, query options, validator),
│                serializer/ (json, xml, metadata), deserializer/ (json, xml),
│                batch, apply, eval, inmemory, adapters/ (asgi, wsgi)
├── client/      client, uri_builder, filters, serialization, transport, responses
└── ext/         sql (filter → SQL WHERE)
```

## 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.
