Skip to content

Commit e3b3608

Browse files
Merge pull request #90 from shcherbak-ai/dev
build: add Python 3.14 support
2 parents a3c251d + c1633b5 commit e3b3608

File tree

16 files changed

+835
-142
lines changed

16 files changed

+835
-142
lines changed

.github/workflows/ci-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
matrix:
2020
os: [ "ubuntu-latest" ]
21-
python-version: ["3.10", "3.11", "3.12", "3.13"]
21+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
2222

2323
defaults:
2424
run:
@@ -83,7 +83,7 @@ jobs:
8383
fi
8484
8585
- name: Upload coverage artifact
86-
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
86+
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.14'
8787
uses: actions/upload-artifact@v4
8888
with:
8989
name: coverage-data

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ For detailed contribution procedures, see `CONTRIBUTING.md`, which covers:
1212

1313
**ContextGem** is a Python LLM framework for extracting structured data from documents using long context windows (not RAG-based).
1414

15-
- **Python**: 3.10-3.13
15+
- **Python**: 3.10-3.14
1616
- **License**: Apache 2.0
1717
- **Package Manager**: `uv`
1818

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55

66
- **Refactor**: Code reorganization that doesn't change functionality but improves structure or maintainability
77

8+
## [0.21.0](https://github.com/shcherbak-ai/contextgem/releases/tag/v0.21.0) - 2026-02-22
9+
### Added
10+
- Python 3.14 support.
11+
12+
### Fixed
13+
- Fixed `Union.__getitem__()` incompatibility on Python 3.14, which caused `JsonObjectConcept` structure validation to fail when using complex union/optional type annotations (e.g., `Optional[Literal[...] | Literal[...]]`).
14+
- Fixed incomplete `types.UnionType` detection in type normalization and validation, ensuring union types created with the `|` operator (Python 3.10+) are handled consistently across Python 3.10–3.14.
15+
816
## [0.20.0](https://github.com/shcherbak-ai/contextgem/releases/tag/v0.20.0) - 2026-02-22
917
### Added
1018
- Auto-generate tool schemas from `@register_tool` decorated functions. Pass functions directly to `tools=[...]` — schemas are built automatically from type hints and docstrings. Explicit OpenAI-compatible schema dicts remain supported for full backward compatibility.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| | |
66
|----------|--------|
7-
| **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
7+
| **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
88
| **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![bandit security](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/bandit-security.yml?branch=main&style=flat-square&label=bandit)](https://github.com/shcherbak-ai/contextgem/actions/workflows/bandit-security.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
99
| **Tools** | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/uv) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff) [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&style=flat-square)](https://pydantic.dev) [![pyright](https://img.shields.io/badge/pyright-checked-blue?style=flat-square)](https://github.com/microsoft/pyright) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-blue?logo=pre-commit&logoColor=white&style=flat-square)](https://github.com/pre-commit/pre-commit) [![deptry](https://img.shields.io/badge/deptry-checked-blue?style=flat-square)](https://github.com/fpgmaas/deptry) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg?style=flat-square)](https://github.com/pypa/hatch) |
1010
| **Docs** | [![docs](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/docs.yml?branch=main&style=flat-square&label=docs)](https://github.com/shcherbak-ai/contextgem/actions/workflows/docs.yml) [![documentation](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://shcherbak-ai.github.io/contextgem/) ![Docstring Coverage](https://contextgem.dev/_static/interrogate-badge.svg) [![DeepWiki](https://img.shields.io/static/v1?label=DeepWiki&message=Chat%20with%20Code&labelColor=%23283593&color=%237E57C2&style=flat-square)](https://deepwiki.com/shcherbak-ai/contextgem) |

contextgem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
ContextGem - Effortless LLM extraction from documents
2121
"""
2222

23-
__version__ = "0.20.0"
23+
__version__ = "0.21.0"
2424
__author__ = "Shcherbak AI AS"
2525

2626
from contextgem.public import (

contextgem/internal/base/concepts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ def _process_structure_value(cls, value: Any, path: str = "") -> Any:
757757
# Handle Optional type hints and ensure they only contain primitive types
758758
elif (
759759
hasattr(value, "__origin__") and getattr(value, "__origin__", None) is Union
760-
):
760+
) or isinstance(value, UnionType):
761761
# Check if it's an Optional (Union with None)
762762
args = get_args(value)
763763
is_optional = type(None) in args

contextgem/internal/typings/strings_to_types.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from __future__ import annotations
2929

30+
from functools import reduce
3031
from typing import Any, Literal, Union
3132

3233
from contextgem.internal.typings.types_to_strings import PRIMITIVE_TYPES_STRING_MAP
@@ -172,10 +173,8 @@ def _parse_type_hint(s: str, i: int = 0) -> tuple[Any, int]:
172173
if len(types_list) == 2:
173174
return Union[types_list[0], types_list[1]], i # noqa: UP007
174175
else:
175-
# For more than two types, unpack them as arguments.
176-
args = (types_list[0], types_list[1]) + tuple(types_list[2:])
177-
# Type checker doesn't recognize Union.__getitem__ method, which works at runtime
178-
union_type = Union.__getitem__(args) # type: ignore
176+
# For more than two types, build a union using the | operator.
177+
union_type = reduce(lambda a, b: a | b, types_list)
179178
return union_type, i
180179

181180
elif ident.lower() == "optional":
@@ -197,9 +196,7 @@ def _parse_type_hint(s: str, i: int = 0) -> tuple[Any, int]:
197196
i = _skip_whitespace(s, i)
198197

199198
# Create union from the collected types
200-
args = tuple(union_types)
201-
# Type checker doesn't recognize Union.__getitem__ method, which works at runtime
202-
inner_type = Union.__getitem__(args) # type: ignore
199+
inner_type = reduce(lambda a, b: a | b, union_types)
203200

204201
i = _skip_whitespace(s, i)
205202
if i >= len(s) or s[i] != "]":

contextgem/internal/typings/types_normalization.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
from __future__ import annotations
2828

29+
from functools import reduce
30+
from types import UnionType
2931
from typing import Any, Dict, List, Optional, Union, get_args, get_origin # noqa: UP035
3032

3133

@@ -96,7 +98,7 @@ def _normalize_type_annotation(tp: Any) -> Any:
9698
_normalize_type_annotation(args[0]), _normalize_type_annotation(args[1])
9799
]
98100

99-
elif origin is Union or origin is Optional:
101+
elif origin is Union or origin is Optional or isinstance(tp, UnionType):
100102
if not args:
101103
return Union if origin is Union else Optional
102104

@@ -114,16 +116,12 @@ def _normalize_type_annotation(tp: Any) -> Any:
114116
if len(normalized_args) == 1:
115117
return normalized_args[0]
116118
else:
117-
return Union[normalized_args] # noqa: UP007
119+
return reduce(lambda a, b: a | b, normalized_args)
118120

119121
# Handle Union
120122
if len(normalized_args) == 1:
121123
return normalized_args[0]
122-
if len(normalized_args) == 2:
123-
return Union[normalized_args[0], normalized_args[1]] # noqa: UP007
124-
# For more than two types, we need to use __getitem__ with tuple
125-
# Type checker doesn't recognize Union.__getitem__ method, which works at runtime
126-
return Union.__getitem__(normalized_args) # type: ignore
124+
return reduce(lambda a, b: a | b, normalized_args)
127125

128126
# For other types with origin/args, keep the general structure
129127
# but normalize the arguments

contextgem/internal/typings/user_type_hints_validation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from __future__ import annotations
2929

30+
from types import UnionType
3031
from typing import Any, Union, get_args, get_origin
3132

3233
from pydantic import BaseModel, ConfigDict, create_model
@@ -75,7 +76,7 @@ def _is_optional(type_hint: Any) -> bool:
7576
:rtype: bool
7677
"""
7778
origin = get_origin(type_hint)
78-
if origin is Union:
79+
if origin is Union or isinstance(type_hint, UnionType):
7980
return type(None) in get_args(type_hint)
8081
return False
8182

dev/readme.template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| | |
66
|----------|--------|
7-
| **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
7+
| **Package** | [![PyPI](https://img.shields.io/pypi/v/contextgem?logo=pypi&label=PyPi&logoColor=gold&style=flat-square)](https://pypi.org/project/contextgem/) [![PyPI Downloads](https://static.pepy.tech/badge/contextgem/month)](https://pepy.tech/projects/contextgem) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue?logo=python&logoColor=gold&style=flat-square)](https://www.python.org/downloads/) [![License](https://img.shields.io/badge/License-Apache_2.0-blue?style=flat-square)](https://opensource.org/licenses/Apache-2.0) |
88
| **Quality** | [![tests](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/ci-tests.yml?branch=main&style=flat-square&label=tests)](https://github.com/shcherbak-ai/contextgem/actions/workflows/ci-tests.yml) [![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/SergiiShcherbak/daaee00e1dfff7a29ca10a922ec3becd/raw/coverage.json&style=flat-square)](https://github.com/shcherbak-ai/contextgem/actions) [![CodeQL](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/codeql.yml?branch=main&style=flat-square&label=CodeQL)](https://github.com/shcherbak-ai/contextgem/actions/workflows/codeql.yml) [![bandit security](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/bandit-security.yml?branch=main&style=flat-square&label=bandit)](https://github.com/shcherbak-ai/contextgem/actions/workflows/bandit-security.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10489/badge?1)](https://www.bestpractices.dev/projects/10489) |
99
| **Tools** | [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json&style=flat-square)](https://github.com/astral-sh/uv) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json&style=flat-square)](https://github.com/astral-sh/ruff) [![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json&style=flat-square)](https://pydantic.dev) [![pyright](https://img.shields.io/badge/pyright-checked-blue?style=flat-square)](https://github.com/microsoft/pyright) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-blue?logo=pre-commit&logoColor=white&style=flat-square)](https://github.com/pre-commit/pre-commit) [![deptry](https://img.shields.io/badge/deptry-checked-blue?style=flat-square)](https://github.com/fpgmaas/deptry) [![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg?style=flat-square)](https://github.com/pypa/hatch) |
1010
| **Docs** | [![docs](https://img.shields.io/github/actions/workflow/status/shcherbak-ai/contextgem/docs.yml?branch=main&style=flat-square&label=docs)](https://github.com/shcherbak-ai/contextgem/actions/workflows/docs.yml) [![documentation](https://img.shields.io/badge/docs-latest-blue?style=flat-square)](https://shcherbak-ai.github.io/contextgem/) ![Docstring Coverage](https://contextgem.dev/_static/interrogate-badge.svg) [![DeepWiki](https://img.shields.io/static/v1?label=DeepWiki&message=Chat%20with%20Code&labelColor=%23283593&color=%237E57C2&style=flat-square)](https://deepwiki.com/shcherbak-ai/contextgem) |

0 commit comments

Comments
 (0)