Bump dependencies & add 3.12/13 support

This commit is contained in:
Andras Schmelczer 2026-10-27 16:17:58 +00:00
commit 6a8d669755
14 changed files with 130 additions and 85 deletions

View file

@ -7,6 +7,7 @@ on:
- 'docs/**' - 'docs/**'
- 'great_ai/**' - 'great_ai/**'
- 'mkdocs.yaml' - 'mkdocs.yaml'
- 'pyproject.toml'
- '.forgejo/workflows/docs.yml' - '.forgejo/workflows/docs.yml'
workflow_dispatch: workflow_dispatch:
@ -29,11 +30,11 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Set up Python - name: Set up Python
run: uv python install 3.10 run: uv python install 3.13
- name: Install dependencies - name: Install dependencies
run: | run: |
uv venv --python 3.10 uv venv --python 3.13
uv pip install '.[dev]' uv pip install '.[dev]'
- name: Build documentation - name: Build documentation

View file

@ -17,7 +17,7 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Set up Python - name: Set up Python
run: uv python install 3.10 run: uv python install 3.13
# Forgejo cannot use PyPI trusted publishing (OIDC), so authenticate with # Forgejo cannot use PyPI trusted publishing (OIDC), so authenticate with
# an API token, the same way the sibling repos publish their packages. # an API token, the same way the sibling repos publish their packages.

View file

@ -24,14 +24,14 @@ jobs:
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Set up Python - name: Set up Python
run: uv python install 3.10 run: uv python install 3.13
- name: Install dependencies - name: Install dependencies
# --seed gives the venv its own pip, which scripts/check-python.sh shells # --seed gives the venv its own pip, which scripts/check-python.sh shells
# out to for its linters; '.[dev]' provides the rest (mypy resolves the # out to for its linters; '.[dev]' provides the rest (mypy resolves the
# package's own imports against the installed tree). # package's own imports against the installed tree).
run: | run: |
uv venv --seed --python 3.10 uv venv --seed --python 3.13
uv pip install '.[dev]' uv pip install '.[dev]'
- name: Check code and formatting - name: Check code and formatting
@ -45,12 +45,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# pyproject declares >= 3.7, but uv's standalone CPython and the current python-version: ['3.12', '3.13']
# dependency floors (scikit-learn/numpy now require >= 3.9) no longer
# build on the older interpreters, so we test the current supported set.
# The windows leg of the old GitHub matrix is dropped: this Forgejo
# instance only has a Linux `docker` runner.
python-version: ['3.10', '3.11', '3.12', '3.13']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.4
FROM python:3.10.4-slim-bullseye FROM python:3.13-slim-trixie
LABEL org.opencontainers.image.title="GreatAI package wrapper image" LABEL org.opencontainers.image.title="GreatAI package wrapper image"
LABEL org.opencontainers.image.vendor="ScoutinScience B.V." LABEL org.opencontainers.image.vendor="ScoutinScience B.V."

View file

@ -59,6 +59,8 @@ However, [research indicates](https://great-ai.scoutinscience.com) that professi
## Find `great-ai` on [PyPI](https://pypi.org/project/great-ai/) ## Find `great-ai` on [PyPI](https://pypi.org/project/great-ai/)
Requires Python 3.12 or later, including Python 3.13.
```sh ```sh
pip install great-ai pip install great-ai
``` ```
@ -78,8 +80,8 @@ Contributions are welcome.
```sh ```sh
python3 -m venv --copies .env python3 -m venv --copies .env
source .env/bin/activate source .env/bin/activate
pip install --upgrade flit pip pip install --upgrade pip
flit install --symlink pip install --editable '.[dev]'
``` ```
### Develop ### Develop

View file

@ -5,18 +5,17 @@ Provided you already have [Python3](https://www.python.org/downloads/){ target=_
```sh ```sh
pip install great-ai pip install great-ai
``` ```
> Python 3.7 or later is required. > Python 3.12 or later is required. Python 3.13 is supported.
This will work on all major operating systems. This will work on all major operating systems.
## Google Colab ## Google Colab
In order to use GreatAI in [Google Colab](https://colab.research.google.com){ target=_blank }, you need to downgrade `pyyaml` to a Colab compatible version. [See related StackOverflow question](https://stackoverflow.com/questions/69564817/typeerror-load-missing-1-required-positional-argument-loader-in-google-col){ target=_blank }. To use GreatAI in [Google Colab](https://colab.research.google.com){ target=_blank }, select a runtime with Python 3.12 or later and install the package:
```sh ```sh
pip install great-ai pyyaml==5.4.1 pip install --upgrade great-ai
``` ```
> This will make GreatAI work in Colab.
## Command-line tools ## Command-line tools
@ -50,4 +49,4 @@ You can also install the latest (usually unreleased) version from GitHub.
```sh ```sh
pip install --upgrade git+https://github.com/schmelczer/great-ai.git pip install --upgrade git+https://github.com/schmelczer/great-ai.git
``` ```
> Python 3.7 or later is required. > Python 3.12 or later is required.

View file

@ -50,7 +50,7 @@ After training and evaluating a model, it is exported using [great_ai.save_model
save_model(model, key='my-domain-predictor') save_model(model, key='my-domain-predictor')
``` ```
For more info, checkout [the configuration how-to page](/how-to-guides/configure-service). For more info, check out [the configuration how-to page](/how-to-guides/configure-service).
### [Deployment notebook](deploy.ipynb) ### [Deployment notebook](deploy.ipynb)

View file

@ -16,8 +16,7 @@ def get_description(
style={"color": accent_color}, style={"color": accent_color},
), ),
dcc.Markdown( dcc.Markdown(
strip_lines( strip_lines(f"""
f"""
> View the live data of your deployment here. > View the live data of your deployment here.
## Using the API ## Using the API
@ -27,8 +26,7 @@ def get_description(
## Details ## Details
{function_docs} {function_docs}
""" """),
),
className="description", className="description",
), ),
] ]

View file

@ -94,11 +94,9 @@ class ConfigFile(Mapping[str, str]):
try: try:
value = next(v for v in values if v) value = next(v for v in values if v)
except StopIteration: except StopIteration:
raise ParseError( raise ParseError(f"""Cannot parse config file ({
f"""Cannot parse config file ({
self._path.absolute() self._path.absolute()
}), error at key `{key}`""" }), error at key `{key}`""")
)
already_exists = key in self._key_values already_exists = key in self._key_values
if already_exists and not value.startswith( if already_exists and not value.startswith(

View file

@ -39,11 +39,9 @@ def manage_communication(
is_iteration_over = True is_iteration_over = True
except Exception as e: except Exception as e:
if ignore_exceptions: if ignore_exceptions:
logger.error( logger.error(f"""Exception {e} encountered in input, traceback:\n{
f"""Exception {e} encountered in input, traceback:\n{
traceback.format_exc() traceback.format_exc()
}""" }""")
)
else: else:
raise raise

View file

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["flit_core >=3.2,<4"] requires = ["flit_core >= 4.1.0, < 5"]
build-backend = "flit_core.buildapi" build-backend = "flit_core.buildapi"
[tool.flit.module] [tool.flit.module]
@ -14,6 +14,8 @@ license = { file = "LICENSE" }
classifiers = [ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed", "Typing :: Typed",
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
@ -21,51 +23,51 @@ classifiers = [
"Natural Language :: English", "Natural Language :: English",
] ]
keywords = ["SE4ML", "MLOps", "AI engineering", "general", "robust", "end-to-end", "automated", "trustworthy", "ai", "deployment"] keywords = ["SE4ML", "MLOps", "AI engineering", "general", "robust", "end-to-end", "automated", "trustworthy", "ai", "deployment"]
requires-python = ">= 3.10" requires-python = ">= 3.12"
dependencies = [ dependencies = [
"scikit-learn >= 1.3, < 2", "scikit-learn >= 1.9.1, < 2",
"matplotlib >= 3.7, < 4", "matplotlib >= 3.11.2, < 4",
"numpy >= 1.24, < 3", "numpy >= 2.5.4, < 3",
"nbconvert >= 7.0, < 8", "nbconvert >= 7.17.2, < 8",
"ipython >= 8.0, < 10", "ipython >= 9.17.1, < 10",
"unidecode >= 1.3.0, < 2", "unidecode >= 1.4.0, < 2",
"syntok >= 1.4.0, < 2", "syntok >= 1.4.4, < 2",
"langcodes[data] >= 3.3.0, < 4", "langcodes[data] >= 3.5.1, < 4",
"langdetect >= 1.0.9, < 2", "langdetect >= 1.0.9, < 2",
"tinydb >= 4.7.0, < 5", "tinydb >= 4.9.0, < 5",
"boto3 >= 1.34, < 2", "boto3 >= 1.43.119, < 2",
"plotly >= 5.20, < 7", "plotly >= 7.1.0, < 8",
"pandas >= 2.0, < 4", "pandas >= 3.1.0, < 4",
"dash >= 2.16, < 5", "dash >= 4.4.1, < 5",
"fastapi >= 0.110, < 1", "fastapi >= 0.162.0, < 1",
"uvicorn[standard] >= 0.27, < 1", "uvicorn[standard] >= 0.54.0, < 1",
"a2wsgi >= 1.9, < 2", "a2wsgi >= 1.10.10, < 2",
"watchdog >= 3.0, < 7", "watchdog >= 6.0.0, < 7",
"typeguard >= 4.0, < 5", "typeguard >= 4.6.0, < 5",
"pydantic >= 2.5, < 3", "pydantic >= 2.14.1, < 3",
"pymongo >= 4.6, < 5", "pymongo >= 4.19.0, < 5",
"dill >= 0.3.6, < 1", "dill >= 0.4.1, < 1",
"tqdm >= 4.64, < 5", "tqdm >= 4.70.1, < 5",
"httpx >= 0.24, < 1", "httpx >= 0.28.1, < 1",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"flit >= 3.9, < 4", "flit >= 4.1.0, < 5",
"mkdocs >= 1.5, < 2", "mkdocs >= 1.6.1, < 2",
"mkdocstrings[python] >= 0.24, < 1", "mkdocstrings[python] >= 1.0.8, < 2",
"mkdocs-material >= 9.5, < 10", "mkdocs-material >= 9.8.0, < 10",
"mkdocs-jupyter >= 0.24, < 1", "mkdocs-jupyter >= 0.27.0, < 1",
"mkdocs-git-revision-date-localized-plugin >= 1.2, < 2", "mkdocs-git-revision-date-localized-plugin >= 1.6.2, < 2",
"autoflake >= 2.0, < 3", "autoflake >= 2.4.0, < 3",
"isort >= 5.12, < 7", "isort >= 9.0.2, < 10",
"black[jupyter] >= 25.1, < 26", "black[jupyter] >= 26.10.1, < 27",
"mypy >= 1.8, < 2", "mypy >= 2.4.0, < 3",
"flake8 >= 7.0, < 8", "flake8 >= 7.4.1, < 8",
"tox >= 4.0, < 5", "tox >= 4.67.2, < 5",
"pytest >= 7.4, < 9", "pytest >= 9.1.1, < 10",
"pytest-cov >= 4.1, < 8", "pytest-cov >= 7.1.0, < 8",
"pytest-asyncio >= 0.23, < 2", "pytest-asyncio >= 1.4.1, < 2",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]

61
tests/test_http_api.py Normal file
View file

@ -0,0 +1,61 @@
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from great_ai import GreatAI, Trace, configure, context
from great_ai.deploy.routes.dashboard.create_dash_app import update_charts
@pytest.mark.parametrize("is_async", [False, True])
def test_service(
is_async: bool, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(context, "_context", None)
configure(version="1.2.3", disable_se4ml_banner=True)
def predict_sync(value: int) -> int:
return value + 2
async def predict_async(value: int) -> int:
return value + 2
service = GreatAI.create(predict_async if is_async else predict_sync)
with TestClient(service.app) as client:
response = client.post("/predict", json={"value": 3})
assert response.status_code == 200
trace = response.json()
assert trace["output"] == 5
assert trace["logged_values"]["arg:value:value"] == 3
trace_url = f"/traces/{trace['trace_id']}"
assert client.get(trace_url).json() == trace
assert client.post("/traces", json={}).json() == [trace]
assert client.post("/predict", json={"value": "invalid"}).status_code == 422
feedback_url = f"{trace_url}/feedback/"
assert client.put(feedback_url, json={"feedback": "correct"}).status_code == 202
assert client.get(feedback_url).json() == "correct"
assert client.delete(feedback_url).status_code == 204
assert client.get(feedback_url).json() is None
health = client.get("/health")
assert health.status_code == 200
assert health.json()["is_healthy"] is True
assert client.get("/version").json()["version"].split("+")[0] == "1.2.3"
assert client.get("/openapi.json").status_code == 200
assert client.get("/docs").status_code == 200
assert client.get("/dashboard/").status_code == 200
assert client.get("/dashboard/_dash-layout").status_code == 200
assert client.get("/assets/index.css").status_code == 200
histogram, coordinates, style = update_charts(
[Trace.model_validate(trace)], accent_color="#123456"
)
assert len(histogram.figure.data) == 1
assert len(coordinates.data) == 1
assert style == {}
assert client.delete(trace_url).status_code == 204
assert client.get(trace_url).status_code == 404

View file

@ -14,14 +14,11 @@ def test_simple() -> None:
assert c.first_key == "András" assert c.first_key == "András"
assert c.second_key == "test 2" assert c.second_key == "test 2"
assert c.third_key == "test= 2==" assert c.third_key == "test= 2=="
assert ( assert c.fourth_key == """
c.fourth_key
== """
this# this#
is is
multiline multiline
""" """
)
assert c.whitespace == "hardly matters" assert c.whitespace == "hardly matters"
with pytest.raises(KeyError): with pytest.raises(KeyError):
c.this c.this
@ -34,14 +31,11 @@ def test_simple_dict() -> None:
assert c["my_hashtag"] == "#great_ai" assert c["my_hashtag"] == "#great_ai"
assert c["second_key"] == "test 2" assert c["second_key"] == "test 2"
assert c["third_key"] == "test= 2==" assert c["third_key"] == "test= 2=="
assert ( assert c["fourth_key"] == """
c["fourth_key"]
== """
this# this#
is is
multiline multiline
""" """
)
assert c["whitespace"] == "hardly matters" assert c["whitespace"] == "hardly matters"
with pytest.raises(KeyError): with pytest.raises(KeyError):
c["#this"] c["#this"]
@ -55,14 +49,11 @@ def test_string_path() -> None:
assert c.second_key == "test 2" assert c.second_key == "test 2"
assert c.third_key == "test= 2==" assert c.third_key == "test= 2=="
assert ( assert c.fourth_key == """
c.fourth_key
== """
this# this#
is is
multiline multiline
""" """
)
assert c.whitespace == "hardly matters" assert c.whitespace == "hardly matters"

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py37,py38,py39,py310 envlist = py312,py313
isolated_build = True isolated_build = True
[testenv] [testenv]