Bump dependencies & add 3.12/13 support
This commit is contained in:
parent
c55eba2077
commit
6a8d669755
14 changed files with 130 additions and 85 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
61
tests/test_http_api.py
Normal 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
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
tox.ini
2
tox.ini
|
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py37,py38,py39,py310
|
envlist = py312,py313
|
||||||
isolated_build = True
|
isolated_build = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue