Modern Python client for the 1C:Enterprise OData (v3) standard interface
Project description
onec-odata
A modern, typed Python client for the 1C:Enterprise OData standard interface
(/odata/standard.odata). Built for Python 3.13+ on top of
httpx.
It exists because generic OData libraries assume OData v4 and choke on 1C, which speaks OData v3 with several of its own conventions. This library handles the 1C reality directly:
- OData v3 typed literals —
guid'...',datetime'...'in URLs and keys (v4 dropped these, which is why other clients send the wrong thing). - Correct
$metadataparsing — a namespace-tolerant CSDL v3 parser that copes with platform-version namespace drift and Cyrillic identifiers. - 1C property conventions —
_Keyreferences,_Typedispatch/composite fields,_Base64Datavalue storage, and the four-underscore____Presentationsuffix. - 1C-only operations — document
Post/Unpost, register virtual tables (SliceLast,Balance,BalanceAndTurnovers, …),allowedOnly, optimistic locking viaDataVersion/If-Match, and the data-load mode header. - A fluent filter DSL — every operator and function from the 1C docs,
including
cast/isoffor composite types andany/alllambdas.
Installation
pip install onec-odata
Usage
Connect
from onec_odata import ODataClient, Query, F, Guid
client = ODataClient("http://host/base", auth=("user", "password"))
The odata/standard.odata path is appended automatically. Use the client as a
context manager to close the underlying connection pool:
with ODataClient("http://host/base", auth=("user", "pass")) as client:
...
Read a collection
goods = client.catalog("Товары") # -> Catalog_Товары
page = goods.list(
Query()
.filter(F("Цена") > 1000)
.select("Ref_Key", "Code", "Description")
.order_by("Description")
.top(50)
.with_count() # $inlinecount=allpages
)
print(page.total_count) # total across all pages
for item in page:
print(item.ref_key, item["Description"])
Read one entity by key
item = goods.get(Guid("41aa6331-954f-11e3-814b-005056c00008"))
# Composite key (e.g. an information register):
import datetime as dt
rate = client.information_register("КурсыВалют").get({
"Period": dt.datetime(2008, 2, 5),
"Валюта_Key": Guid("9d5c4222-8c4c-11db-a9b0-00055d49b45e"),
})
Stream every match (transparent paging)
for item in goods.iterate(Query().filter(F("DeletionMark") == False), page_size=500):
...
Count
n = goods.count(Query().filter(F("Цена") > 500))
Filters
from onec_odata import F, and_, or_, cast, isof, startswith
# Comparisons and boolean composition (& and |):
q = Query().filter((F("Цена") > 1000) & (F("Цена") < 5000))
# String functions:
Query().filter(startswith("Производитель", "ООО"))
# Navigation through references:
Query().filter(F("Контрагент/ИНН") == "7700000000").order_by("Контрагент/ИНН")
# Composite (multi-type) attribute compared to a typed reference:
Query().filter(
F("ДокументПрихода") == cast(Guid("0d4a79cb-9843-4147-bcd9-80ac3ca2b9c7"),
"Document_ПриходнаяНакладная")
)
# Lambda over a tabular section: documents with any line priced over 10000
Query().filter(F("Товары").any(lambda d: d.nav("Цена") > 10000))
Create, update, delete
created = goods.create({
"Description": "Шлепанцы",
"Артикул": "SL56X",
"Поставщик_Key": Guid("086715b0-f348-11db-a9c5-00055d49b45e"),
})
# PATCH — only the given properties change:
goods.update(created.ref_key, {"Description": "Новое имя"})
# PUT — full replace; references use the @odata.bind form:
goods.replace(created.ref_key, {
"Description": "Шлепанцы",
"Поставщик@odata.bind": "Catalog_Поставщики(guid'...')",
...
})
# Optimistic locking:
goods.update(item.ref_key, {...}, if_match=item.data_version)
goods.delete(created.ref_key) # immediate delete, not a deletion mark
Documents
docs = client.document("РасходТовара")
docs.post_document(doc_key, operational=False) # провести
docs.unpost_document(doc_key) # отмена проведения
Register virtual tables (functions)
reg = client.information_register("КурсыВалют")
slice_last = reg.call("SliceLast", {"Condition": "Валюта/ОсновнаяВалюта_Key eq guid'...'"})
acc = client.accumulation_register("ТоварныеЗапасы")
turnovers = acc.call("BalanceAndTurnovers", {
"StartPeriod": dt.datetime(2014, 1, 1),
"EndPeriod": dt.datetime(2014, 2, 1),
"Condition": "Товар_Key eq guid'...'",
})
Metadata
meta = client.metadata() # parsed once, then cached
et = meta.entity_type_for_set("Catalog_Товары")
for prop in et.properties:
print(prop.name, prop.type, "key" if prop.name in et.key else "")
Error handling
from onec_odata import EntityNotFoundError, ConcurrencyError, AccessDeniedError
try:
goods.get(some_key)
except EntityNotFoundError as e:
print(e.status_code, e.internal_code, e.message)
Every 1C internal error code (section 17.4.10 of the docs) is mapped onto a
specific exception subclass where it makes sense, with the raw code available
as error.internal_code.
Debugging — see the actual OData request
Pass debug=True to print every request (decoded, so Cyrillic and the
$filter operators are readable) to stderr:
client = ODataClient("http://host/base", auth=("user", "pass"), debug=True)
goods.list(Query().filter(F("Posted") == True).select("Ref_Key", "Number").top(3))
# [onec-odata] GET http://host/base/odata/standard.odata/Document_Заём?$filter=Posted eq true&$select=Ref_Key,Number&$top=3 -> 200 (212 ms)
debug can also be a callback receiving a RequestDebug (method, decoded
url, raw_url, redacted headers, body, status_code, elapsed_ms):
client.debug = lambda info: my_logger.info("%s %s", info.method, info.url)
Or route it through logging (requests are always logged at DEBUG):
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("onec_odata").setLevel(logging.DEBUG)
Preview a URL without sending it, or inspect the last exchange:
print(goods.url(Query().filter(F("Цена") > 1000).select("Ref_Key").top(5)))
# http://host/base/odata/standard.odata/Catalog_Товары?$filter=Цена gt 1000&$select=Ref_Key&$top=5
client.last_request # the raw httpx.Request that was sent
client.last_response # the raw httpx.Response
Sensitive headers (Authorization, Cookie) are redacted in debug output.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check src tests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file onec_odata-0.1.0.tar.gz.
File metadata
- Download URL: onec_odata-0.1.0.tar.gz
- Upload date:
- Size: 32.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d62626be2bcf9ce2b6a7a727e2530fb9a03c2f04d43b2879d5990d747146fb1
|
|
| MD5 |
2c5ed3b2ed6b15b7dd210e7c950ec8ef
|
|
| BLAKE2b-256 |
6483b75a0adcf8faf333c2df82179d99b71b0ba488f0bc6b65c40a1a69d6c4c4
|
File details
Details for the file onec_odata-0.1.0-py3-none-any.whl.
File metadata
- Download URL: onec_odata-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c30f0c8bc182969b9136fc277aa0339cb07ca766d886f5567114a0198ee5679
|
|
| MD5 |
598b860082e87f31c4d3009c5178f9db
|
|
| BLAKE2b-256 |
9de6f04f94b05969bf6509c0a379288622085bb349a7d1b0fc70152559895b5b
|