odatapy 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odatapy/__init__.py +30 -0
- odatapy/client/__init__.py +132 -0
- odatapy/client/auth.py +159 -0
- odatapy/client/batch.py +628 -0
- odatapy/client/client.py +1829 -0
- odatapy/client/filters.py +972 -0
- odatapy/client/responses.py +304 -0
- odatapy/client/serialization.py +1257 -0
- odatapy/client/transport.py +505 -0
- odatapy/client/uri_builder.py +328 -0
- odatapy/data/__init__.py +426 -0
- odatapy/edm/__init__.py +91 -0
- odatapy/edm/annotations.py +742 -0
- odatapy/edm/csdl.py +430 -0
- odatapy/edm/geo.py +385 -0
- odatapy/edm/json_csdl.py +462 -0
- odatapy/edm/json_value.py +377 -0
- odatapy/edm/model.py +1276 -0
- odatapy/edm/names.py +27 -0
- odatapy/edm/primitives.py +1247 -0
- odatapy/edm/xml_io.py +529 -0
- odatapy/errors.py +196 -0
- odatapy/ext/__init__.py +26 -0
- odatapy/ext/sql.py +726 -0
- odatapy/ext/sqlalchemy.py +298 -0
- odatapy/http/__init__.py +23 -0
- odatapy/http/content_type.py +309 -0
- odatapy/http/header.py +41 -0
- odatapy/http/method.py +18 -0
- odatapy/http/prefer.py +292 -0
- odatapy/http/status.py +84 -0
- odatapy/py.typed +0 -0
- odatapy/server/__init__.py +20 -0
- odatapy/server/adapters/__init__.py +11 -0
- odatapy/server/adapters/asgi.py +147 -0
- odatapy/server/adapters/common.py +212 -0
- odatapy/server/adapters/wsgi.py +187 -0
- odatapy/server/apply.py +740 -0
- odatapy/server/batch.py +818 -0
- odatapy/server/default_processor.py +131 -0
- odatapy/server/deserializer/__init__.py +6 -0
- odatapy/server/deserializer/json_deserializer.py +648 -0
- odatapy/server/deserializer/xml_deserializer.py +591 -0
- odatapy/server/dispatcher.py +734 -0
- odatapy/server/eval.py +994 -0
- odatapy/server/handler.py +658 -0
- odatapy/server/inmemory.py +3164 -0
- odatapy/server/negotiator.py +280 -0
- odatapy/server/odata.py +77 -0
- odatapy/server/processor.py +492 -0
- odatapy/server/request.py +220 -0
- odatapy/server/serializer/__init__.py +24 -0
- odatapy/server/serializer/factory.py +45 -0
- odatapy/server/serializer/json_serializer.py +1222 -0
- odatapy/server/serializer/metadata.py +713 -0
- odatapy/server/serializer/xml_serializer.py +859 -0
- odatapy/server/support.py +319 -0
- odatapy/server/uri/__init__.py +106 -0
- odatapy/server/uri/apply_parser.py +552 -0
- odatapy/server/uri/expression.py +440 -0
- odatapy/server/uri/expression_parser.py +1302 -0
- odatapy/server/uri/parser.py +1525 -0
- odatapy/server/uri/query_options.py +351 -0
- odatapy/server/uri/resource.py +317 -0
- odatapy/server/uri/search_parser.py +162 -0
- odatapy/server/uri/validator.py +387 -0
- odatapy-5.0.0.dist-info/METADATA +313 -0
- odatapy-5.0.0.dist-info/RECORD +71 -0
- odatapy-5.0.0.dist-info/WHEEL +4 -0
- odatapy-5.0.0.dist-info/licenses/LICENSE +202 -0
- odatapy-5.0.0.dist-info/licenses/NOTICE +7 -0
odatapy/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""odatapy — an OData v4.0 / v4.01 server and client library for Python.
|
|
2
|
+
|
|
3
|
+
A port of Apache Olingo OData 5.0.0 (Java) to idiomatic Python.
|
|
4
|
+
|
|
5
|
+
Quick start (server):
|
|
6
|
+
|
|
7
|
+
from odatapy.server import OData
|
|
8
|
+
from odatapy.server.inmemory import InMemoryDataStore, InMemoryProcessor
|
|
9
|
+
|
|
10
|
+
odata = OData.new_instance()
|
|
11
|
+
service_metadata = odata.create_service_metadata(MyProvider())
|
|
12
|
+
handler = odata.create_handler(service_metadata)
|
|
13
|
+
handler.register(InMemoryProcessor(store))
|
|
14
|
+
# wrap with odatapy.server.adapters.ODataASGIApp / ODataWSGIApp
|
|
15
|
+
|
|
16
|
+
Quick start (client):
|
|
17
|
+
|
|
18
|
+
from odatapy.client import ODataClient
|
|
19
|
+
from odatapy.client.transport import HttpxTransport
|
|
20
|
+
|
|
21
|
+
client = ODataClient("http://host/svc", HttpxTransport())
|
|
22
|
+
products = client.get_entity_set(client.entity_set_uri("Products").filter("Price gt 10"))
|
|
23
|
+
|
|
24
|
+
See docs/OLINGO_COMPARISON.md for what is implemented, what is deliberately not
|
|
25
|
+
ported from Apache Olingo, and the known limitations.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__version__ = "5.0.0"
|
|
29
|
+
|
|
30
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""OData client.
|
|
2
|
+
|
|
3
|
+
Build request URIs with :class:`URIBuilder`, execute them through a pluggable
|
|
4
|
+
:class:`~odatapy.client.transport.Transport` (httpx by default, or an in-process
|
|
5
|
+
handler), and get back payload objects. :class:`ODataClient` is the entry point
|
|
6
|
+
and :class:`AsyncODataClient` its asyncio twin; the EDM-enabled variants read
|
|
7
|
+
``$metadata`` so payloads come back as canonical Python values.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from odatapy.client.auth import (
|
|
11
|
+
Auth,
|
|
12
|
+
BasicAuth,
|
|
13
|
+
BearerAuth,
|
|
14
|
+
NoAuth,
|
|
15
|
+
OAuth2ClientCredentialsAuth,
|
|
16
|
+
)
|
|
17
|
+
from odatapy.client.batch import (
|
|
18
|
+
BatchItem,
|
|
19
|
+
BatchPartResponse,
|
|
20
|
+
BatchRequest,
|
|
21
|
+
BatchResponse,
|
|
22
|
+
ChangeSetBuilder,
|
|
23
|
+
)
|
|
24
|
+
from odatapy.client.client import (
|
|
25
|
+
AsyncEdmEnabledODataClient,
|
|
26
|
+
AsyncODataClient,
|
|
27
|
+
AsyncOperation,
|
|
28
|
+
AwaitableOperation,
|
|
29
|
+
EdmEnabledODataClient,
|
|
30
|
+
ODataClient,
|
|
31
|
+
RequestSpec,
|
|
32
|
+
build_prefer,
|
|
33
|
+
)
|
|
34
|
+
from odatapy.client.filters import (
|
|
35
|
+
Field,
|
|
36
|
+
Filter,
|
|
37
|
+
FilterArgFactory,
|
|
38
|
+
FilterFactory,
|
|
39
|
+
OrderBy,
|
|
40
|
+
Search,
|
|
41
|
+
SearchFactory,
|
|
42
|
+
and_,
|
|
43
|
+
it,
|
|
44
|
+
literal,
|
|
45
|
+
not_,
|
|
46
|
+
or_,
|
|
47
|
+
root,
|
|
48
|
+
)
|
|
49
|
+
from odatapy.client.responses import (
|
|
50
|
+
ClientResponse,
|
|
51
|
+
MediaResponse,
|
|
52
|
+
NotModified,
|
|
53
|
+
ODataClientError,
|
|
54
|
+
ODataInvokeResult,
|
|
55
|
+
ODataServerError,
|
|
56
|
+
ODataUpdateResult,
|
|
57
|
+
RawValue,
|
|
58
|
+
StreamedResponse,
|
|
59
|
+
)
|
|
60
|
+
from odatapy.client.serialization import ServiceDocument, ServiceDocumentEntry
|
|
61
|
+
from odatapy.client.transport import (
|
|
62
|
+
AsyncHandlerTransport,
|
|
63
|
+
AsyncHttpxTransport,
|
|
64
|
+
AsyncPolicyTransport,
|
|
65
|
+
AsyncTransport,
|
|
66
|
+
HandlerTransport,
|
|
67
|
+
HttpxTransport,
|
|
68
|
+
PolicyTransport,
|
|
69
|
+
RetryPolicy,
|
|
70
|
+
Transport,
|
|
71
|
+
)
|
|
72
|
+
from odatapy.client.uri_builder import URIBuilder
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
# clients
|
|
76
|
+
"ODataClient",
|
|
77
|
+
"AsyncODataClient",
|
|
78
|
+
"EdmEnabledODataClient",
|
|
79
|
+
"AsyncEdmEnabledODataClient",
|
|
80
|
+
"AsyncOperation",
|
|
81
|
+
"AwaitableOperation",
|
|
82
|
+
"RequestSpec",
|
|
83
|
+
"build_prefer",
|
|
84
|
+
# URIs and expressions
|
|
85
|
+
"URIBuilder",
|
|
86
|
+
"Field",
|
|
87
|
+
"Filter",
|
|
88
|
+
"FilterFactory",
|
|
89
|
+
"FilterArgFactory",
|
|
90
|
+
"OrderBy",
|
|
91
|
+
"Search",
|
|
92
|
+
"SearchFactory",
|
|
93
|
+
"and_",
|
|
94
|
+
"or_",
|
|
95
|
+
"not_",
|
|
96
|
+
"it",
|
|
97
|
+
"root",
|
|
98
|
+
"literal",
|
|
99
|
+
# transports and auth
|
|
100
|
+
"Transport",
|
|
101
|
+
"AsyncTransport",
|
|
102
|
+
"HandlerTransport",
|
|
103
|
+
"AsyncHandlerTransport",
|
|
104
|
+
"HttpxTransport",
|
|
105
|
+
"AsyncHttpxTransport",
|
|
106
|
+
"PolicyTransport",
|
|
107
|
+
"AsyncPolicyTransport",
|
|
108
|
+
"RetryPolicy",
|
|
109
|
+
"Auth",
|
|
110
|
+
"NoAuth",
|
|
111
|
+
"BasicAuth",
|
|
112
|
+
"BearerAuth",
|
|
113
|
+
"OAuth2ClientCredentialsAuth",
|
|
114
|
+
# batch
|
|
115
|
+
"BatchRequest",
|
|
116
|
+
"BatchResponse",
|
|
117
|
+
"BatchItem",
|
|
118
|
+
"BatchPartResponse",
|
|
119
|
+
"ChangeSetBuilder",
|
|
120
|
+
# responses
|
|
121
|
+
"ClientResponse",
|
|
122
|
+
"MediaResponse",
|
|
123
|
+
"NotModified",
|
|
124
|
+
"RawValue",
|
|
125
|
+
"StreamedResponse",
|
|
126
|
+
"ODataInvokeResult",
|
|
127
|
+
"ODataUpdateResult",
|
|
128
|
+
"ODataClientError",
|
|
129
|
+
"ODataServerError",
|
|
130
|
+
"ServiceDocument",
|
|
131
|
+
"ServiceDocumentEntry",
|
|
132
|
+
]
|
odatapy/client/auth.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Authentication helpers for the client transports.
|
|
2
|
+
|
|
3
|
+
An :class:`Auth` adds credentials to outgoing request headers and may react to a
|
|
4
|
+
``401 Unauthorized`` by refreshing them (the transport then retries once)::
|
|
5
|
+
|
|
6
|
+
transport = HttpxTransport(auth=BasicAuth("alice", "secret"))
|
|
7
|
+
transport = HttpxTransport(auth=BearerAuth("eyJ..."))
|
|
8
|
+
transport = HttpxTransport(
|
|
9
|
+
auth=OAuth2ClientCredentialsAuth("https://idp/token", "client-id", "client-secret")
|
|
10
|
+
)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Callable, Protocol
|
|
18
|
+
|
|
19
|
+
from odatapy.client.responses import ClientResponse
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Auth(Protocol):
|
|
23
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
24
|
+
"""Add credentials to the request headers (in place)."""
|
|
25
|
+
|
|
26
|
+
def on_unauthorized(self, response: ClientResponse) -> bool:
|
|
27
|
+
"""Called on a 401; return True to refresh credentials and retry once."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoAuth:
|
|
31
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def on_unauthorized(self, response: ClientResponse) -> bool:
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BasicAuth:
|
|
39
|
+
"""HTTP Basic authentication."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, username: str, password: str) -> None:
|
|
42
|
+
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii")
|
|
43
|
+
self._header = f"Basic {token}"
|
|
44
|
+
|
|
45
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
46
|
+
headers["Authorization"] = self._header
|
|
47
|
+
|
|
48
|
+
def on_unauthorized(self, response: ClientResponse) -> bool:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BearerAuth:
|
|
53
|
+
"""``Authorization: Bearer <token>``; ``token`` may be a string or a callable
|
|
54
|
+
returning the current token (called on every request and after a 401)."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, token: str | Callable[[], str]) -> None:
|
|
57
|
+
self._token = token
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def token(self) -> str:
|
|
61
|
+
return self._token() if callable(self._token) else self._token
|
|
62
|
+
|
|
63
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
64
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
65
|
+
|
|
66
|
+
def on_unauthorized(self, response: ClientResponse) -> bool:
|
|
67
|
+
# A callable token source can produce a fresh token; a static one cannot.
|
|
68
|
+
return callable(self._token)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OAuth2ClientCredentialsAuth:
|
|
72
|
+
"""OAuth 2.0 client-credentials grant.
|
|
73
|
+
|
|
74
|
+
Fetches an access token from ``token_url`` (form-encoded POST) on first use,
|
|
75
|
+
refreshes it when ``expires_in`` has elapsed, and once more after a 401.
|
|
76
|
+
``token_fetcher`` can replace the HTTP call (it must return the token
|
|
77
|
+
response dict) - handy for tests or non-standard identity providers.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
token_url: str,
|
|
83
|
+
client_id: str,
|
|
84
|
+
client_secret: str,
|
|
85
|
+
scope: str | None = None,
|
|
86
|
+
*,
|
|
87
|
+
token_fetcher: Callable[[], dict[str, Any]] | None = None,
|
|
88
|
+
extra_params: dict[str, str] | None = None,
|
|
89
|
+
leeway: float = 30.0,
|
|
90
|
+
httpx_kwargs: dict[str, Any] | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self.token_url = token_url
|
|
93
|
+
self.client_id = client_id
|
|
94
|
+
self.client_secret = client_secret
|
|
95
|
+
self.scope = scope
|
|
96
|
+
self.extra_params = dict(extra_params or {})
|
|
97
|
+
self.leeway = leeway
|
|
98
|
+
self._fetcher = token_fetcher
|
|
99
|
+
self._httpx_kwargs = httpx_kwargs or {}
|
|
100
|
+
self._access_token: str | None = None
|
|
101
|
+
self._expires_at: float | None = None
|
|
102
|
+
self._token_type = "Bearer"
|
|
103
|
+
|
|
104
|
+
# --- token management ------------------------------------------------
|
|
105
|
+
@property
|
|
106
|
+
def access_token(self) -> str | None:
|
|
107
|
+
return self._access_token
|
|
108
|
+
|
|
109
|
+
def is_expired(self) -> bool:
|
|
110
|
+
if self._access_token is None:
|
|
111
|
+
return True
|
|
112
|
+
if self._expires_at is None:
|
|
113
|
+
return False
|
|
114
|
+
return time.monotonic() >= self._expires_at - self.leeway
|
|
115
|
+
|
|
116
|
+
def fetch_token(self) -> str:
|
|
117
|
+
data = self._fetcher() if self._fetcher is not None else self._request_token()
|
|
118
|
+
token = data.get("access_token")
|
|
119
|
+
if not token:
|
|
120
|
+
raise ValueError("The token endpoint did not return an access_token.")
|
|
121
|
+
self._access_token = str(token)
|
|
122
|
+
self._token_type = str(data.get("token_type") or "Bearer")
|
|
123
|
+
expires_in = data.get("expires_in")
|
|
124
|
+
self._expires_at = (
|
|
125
|
+
time.monotonic() + float(expires_in) if expires_in is not None else None
|
|
126
|
+
)
|
|
127
|
+
return self._access_token
|
|
128
|
+
|
|
129
|
+
def _request_token(self) -> dict[str, Any]:
|
|
130
|
+
import httpx
|
|
131
|
+
|
|
132
|
+
form = {
|
|
133
|
+
"grant_type": "client_credentials",
|
|
134
|
+
"client_id": self.client_id,
|
|
135
|
+
"client_secret": self.client_secret,
|
|
136
|
+
**self.extra_params,
|
|
137
|
+
}
|
|
138
|
+
if self.scope:
|
|
139
|
+
form["scope"] = self.scope
|
|
140
|
+
response = httpx.post(self.token_url, data=form, **self._httpx_kwargs)
|
|
141
|
+
response.raise_for_status()
|
|
142
|
+
return response.json()
|
|
143
|
+
|
|
144
|
+
def invalidate(self) -> None:
|
|
145
|
+
self._access_token = None
|
|
146
|
+
self._expires_at = None
|
|
147
|
+
|
|
148
|
+
# --- Auth protocol ---------------------------------------------------
|
|
149
|
+
def apply(self, headers: dict[str, str]) -> None:
|
|
150
|
+
if self.is_expired():
|
|
151
|
+
self.fetch_token()
|
|
152
|
+
headers["Authorization"] = f"{self._token_type} {self._access_token}"
|
|
153
|
+
|
|
154
|
+
def on_unauthorized(self, response: ClientResponse) -> bool:
|
|
155
|
+
self.invalidate()
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = ["Auth", "BasicAuth", "BearerAuth", "NoAuth", "OAuth2ClientCredentialsAuth"]
|