Closes the last open item from the Informix 12 field report. The driver
hardcodes several wire-framing choices that SQLI actually negotiates.
Those choices are correct on every server we've measured, but "correct
as far as we know" and "checked" are different things, and a framing
mismatch corrupts rows silently.
We were already sending SQ_PROTOCOLS with the same 8-byte client offer
IBM's JDBC driver uses, and discarding the reply. Now we decode it.
New informix_db.ServerCapabilities, reachable from any connection:
conn.server_capabilities.four_byte_offset
conn.server_capabilities.varchar_var_len
conn.server_capabilities.violated_assumptions() # [] when we agree
conn.server_version
violated_assumptions() names each place we emit or parse a fixed wire
shape that is actually capability-gated. Non-empty at connect time logs
a warning naming the bit, so an untested server produces a diagnosable
complaint instead of quiet corruption.
Nothing branches on these bits yet — this is observation and validation
only. The hardcoded framing is correct on all three supported servers,
and rewriting working parse paths to be conditional without a server
that needs it trades certainty for risk.
The measurement, and why 12/14/15 are interchangeable:
15.0.1.0.3 bdbe9ffe7fb7ffef ff
14.10.FC7W1 bdbe9ffe7fb7ffef f8
12.10.FC12W1DE bdbe9ffe7fb7ffef f0
^^^^^^^^^^^^^^^^ identical
The first 64 bits are byte-identical. Those releases don't merely behave
alike, they negotiate exactly the same capability set.
Two details worth recording. The reply is NINE bytes; JDBC's
enhancedProtocolMechanism switches on case 0..7 and drops the ninth, so
its BitSet(64) never sees it — yet that dropped byte is the only part
that differs between releases. And Cap_1 in the login response is not a
server version, it's the client's declared protocol level echoed back,
which is why JDBC tests == 316 rather than >=. The version string there
is the internal one: 12.10 reports 9.56, 14.10 reports 9.59. At the
protocol level both really are 9.x servers.
This also retires isUSVER as a red herring: it's one of six bits JDBC
pre-sets for any non-zero Cap_1, and Java's BitSet.set never clears, so
it is true on every modern server regardless of the mask.
Separately, fixed __version__. The distribution was renamed informix-db
-> informix-driver on 2026-05-08 but __init__ kept looking up the old
name. importlib.metadata needs the distribution name, not the module
name, and the miss fails silently — so every install of the renamed
package has reported "0.0.0+local". It escaped notice because a stale
informix-db distribution lingered in the dev venv and answered the
query. tests/test_package_metadata.py now pins the name against
[project].name; verified it catches the bug by reintroducing it.
247/247 integration on 15, 14.10, and 12.10. 120 unit tests.
112 lines
3.8 KiB
TOML
112 lines
3.8 KiB
TOML
[project]
|
|
name = "informix-driver"
|
|
version = "2026.08.27"
|
|
description = "Pure-Python driver for IBM Informix IDS — speaks the SQLI wire protocol over raw sockets. No CSDK, no JVM, no native libraries."
|
|
readme = "README.md"
|
|
license = { text = "MIT" }
|
|
authors = [{ name = "Ryan Malloy", email = "ryan@supported.systems" }]
|
|
requires-python = ">=3.10"
|
|
keywords = ["informix", "database", "sqli", "db-api", "pep-249", "asyncio", "async"]
|
|
classifiers = [
|
|
"Development Status :: 5 - Production/Stable",
|
|
"Framework :: AsyncIO",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: MIT License",
|
|
"Operating System :: OS Independent",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3 :: Only",
|
|
"Programming Language :: Python :: 3.10",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Programming Language :: Python :: 3.13",
|
|
"Programming Language :: Python :: 3.14",
|
|
"Topic :: Database",
|
|
"Topic :: Database :: Front-Ends",
|
|
"Typing :: Typed",
|
|
]
|
|
dependencies = []
|
|
|
|
[project.urls]
|
|
Homepage = "https://informix-driver.warehack.ing"
|
|
Documentation = "https://informix-driver.warehack.ing"
|
|
Source = "https://git.supported.systems/warehack.ing/informix-db"
|
|
Issues = "https://git.supported.systems/warehack.ing/informix-db/issues"
|
|
Changelog = "https://git.supported.systems/warehack.ing/informix-db/src/branch/main/CHANGELOG.md"
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.0",
|
|
"ruff>=0.6",
|
|
]
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["src/informix_db"]
|
|
|
|
[tool.hatch.build.targets.sdist]
|
|
# Defense in depth: exclude operator-private and dev-only artifacts from the sdist
|
|
# (the wheel doesn't ship these by default, but the sdist would).
|
|
# See ~/.claude/rules/python.md for the full pre-publish PII audit playbook.
|
|
exclude = [
|
|
"CLAUDE.md", # operator-private context
|
|
".env", ".env.local", ".env.*",
|
|
".mcp.json", # may contain local filesystem paths
|
|
"build/", # decompiled JDBC, downloaded JARs
|
|
"audits/",
|
|
"docs/**", # protocol notes / decision log / captures — go to GitHub for the depth
|
|
"docs-site/**", # Astro/Starlight site sources — published at informix-driver.warehack.ing
|
|
"tests/reference/**", # Java reference client — spike infra
|
|
"tests/benchmarks/.results/**", # pytest-benchmark cache (gitignored, but still ships unless excluded)
|
|
".pytest_cache/", ".ruff_cache/", ".mypy_cache/",
|
|
"dist/", "*.egg-info/",
|
|
]
|
|
|
|
[tool.ruff]
|
|
line-length = 100
|
|
target-version = "py310"
|
|
src = ["src", "tests"]
|
|
|
|
[tool.ruff.lint]
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # pyflakes
|
|
"I", # isort (import sorting)
|
|
"B", # flake8-bugbear
|
|
"C4", # flake8-comprehensions
|
|
"UP", # pyupgrade
|
|
"SIM", # flake8-simplify
|
|
"PTH", # flake8-use-pathlib
|
|
"RUF", # ruff-specific
|
|
]
|
|
ignore = [
|
|
"E501", # line too long — handled by formatter
|
|
]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"tests/**" = ["B011"] # allow assert False in tests
|
|
|
|
[tool.pytest.ini_options]
|
|
minversion = "8.0"
|
|
testpaths = ["tests"]
|
|
asyncio_mode = "auto" # pytest-asyncio: auto-detect ``async def`` tests
|
|
addopts = [
|
|
"-ra", # short summary for non-passing
|
|
"--strict-markers",
|
|
"--strict-config",
|
|
"-m", "not integration and not benchmark", # default: unit-only. Override with: pytest -m integration / -m benchmark
|
|
]
|
|
markers = [
|
|
"integration: requires a running Informix container (docker compose up); skipped by default",
|
|
"benchmark: pytest-benchmark performance test; skipped by default. Run with `make bench`.",
|
|
]
|
|
|
|
[dependency-groups]
|
|
dev = [
|
|
"pytest-asyncio>=1.3.0",
|
|
"pytest-benchmark>=5.2.3",
|
|
]
|