diff --git a/docs/how-to-guides/install.md b/docs/how-to-guides/install.md index de92ff4..84d0058 100644 --- a/docs/how-to-guides/install.md +++ b/docs/how-to-guides/install.md @@ -5,7 +5,7 @@ Provided you already have [Python3](https://www.python.org/downloads/){ target=_ ```sh pip install great-ai ``` -> Python 3.8 or later is required. +> Python 3.7 or later is required. This will work on all major operating systems. diff --git a/docs/index.md b/docs/index.md index af536d0..9e63ed5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,6 +41,7 @@ Applying AI is becoming increasingly easier but many case studies have shown tha - [x] Docker support for deployment - [x] Deployable Jupyter Notebooks - [x] Dashboard for online monitoring and analysing traces +- [x] Active support for Python 3.7, 3.8, 3.9, and 3.10 ## Roadmap diff --git a/great_ai/deploy/great_ai.py b/great_ai/deploy/great_ai.py index c784548..9516169 100644 --- a/great_ai/deploy/great_ai.py +++ b/great_ai/deploy/great_ai.py @@ -15,13 +15,13 @@ from typing import ( overload, ) -from async_lru import alru_cache from fastapi import FastAPI from tqdm.cli import tqdm from typing_extensions import Literal # <= Python 3.7 from ..constants import DASHBOARD_PATH from ..context import get_context +from ..external.async_lru import alru_cache from ..helper import freeze_arguments, get_function_metadata_store, snake_case_to_text from ..models.use_model import model_versions from ..parameters.automatically_decorate_parameters import ( diff --git a/great_ai/deploy/routes/dashboard/create_dash_app.py b/great_ai/deploy/routes/dashboard/create_dash_app.py index 6ad1ade..2f4bd32 100644 --- a/great_ai/deploy/routes/dashboard/create_dash_app.py +++ b/great_ai/deploy/routes/dashboard/create_dash_app.py @@ -34,6 +34,41 @@ def create_dash_app(function_name: str, version: str, function_docs: str) -> Fla ], ) + execution_time_histogram_container = html.Div() + configuration_container = html.Div( + className="configuration-container", + ) + table = get_traces_table() + traces_table_container = html.Div( + [ + html.Header( + [ + html.H2("Latest traces"), + html.P( + "Recent traces and aggregated metrics are presented below. Try filtering the table." + ), + html.A( + "Filtering syntax.", + href="https://dash.plotly.com/datatable/filtering", + target="_blank", + ), + ] + ), + table, + ], + className="traces-table-container", + ) + parallel_coordinates = dcc.Graph( + className="parallel-coordinates", config={"displaylogo": False} + ) + interval = dcc.Interval( + interval=2 * 1000, # in milliseconds + n_intervals=0, + max_intervals=1 + if get_context().is_production + else -1, # will be incremented in production upon each successful request + ) + app.layout = html.Main( [ html.Div( @@ -49,43 +84,15 @@ def create_dash_app(function_name: str, version: str, function_docs: str) -> Fla function_docs=function_docs, accent_color=accent_color, ), - execution_time_histogram_container := html.Div(), + execution_time_histogram_container, ], ), - configuration_container := html.Div( - className="configuration-container", - ), - traces_table_container := html.Div( - [ - html.Header( - [ - html.H2("Latest traces"), - html.P( - "Recent traces and aggregated metrics are presented below. Try filtering the table." - ), - html.A( - "Filtering syntax.", - href="https://dash.plotly.com/datatable/filtering", - target="_blank", - ), - ] - ), - table := get_traces_table(), - ], - className="traces-table-container", - ), - parallel_coordinates := dcc.Graph( - className="parallel-coordinates", config={"displaylogo": False} - ), + configuration_container, + traces_table_container, + parallel_coordinates, html.Div(className="space-filler"), get_footer(), - interval := dcc.Interval( - interval=2 * 1000, # in milliseconds - n_intervals=0, - max_intervals=1 - if get_context().is_production - else -1, # will be incremented in production upon each successful request - ), + interval, ] ) diff --git a/great_ai/large_file/helper/__init__.py b/great_ai/large_file/helper/__init__.py index 9639296..5eb33e8 100644 --- a/great_ai/large_file/helper/__init__.py +++ b/great_ai/large_file/helper/__init__.py @@ -1,2 +1,3 @@ +from .cached_property import cached_property from .human_readable_to_byte import human_readable_to_byte from .progress_bar import DownloadProgressBar, UploadProgressBar diff --git a/great_ai/large_file/helper/cached_property.py b/great_ai/large_file/helper/cached_property.py new file mode 100644 index 0000000..d1b8bb0 --- /dev/null +++ b/great_ai/large_file/helper/cached_property.py @@ -0,0 +1,52 @@ +from threading import RLock + +_NOT_FOUND = object() + + +class cached_property: + def __init__(self, func): # type: ignore + self.func = func + self.attrname = None + self.__doc__ = func.__doc__ + self.lock = RLock() + + def __set_name__(self, owner, name): # type: ignore + if self.attrname is None: + self.attrname = name + elif name != self.attrname: + raise TypeError( + "Cannot assign the same cached_property to two different names " + f"({self.attrname!r} and {name!r})." + ) + + def __get__(self, instance, owner=None): # type: ignore + if instance is None: + return self + if self.attrname is None: + raise TypeError( + "Cannot use cached_property instance without calling __set_name__ on it." + ) + try: + cache = instance.__dict__ + except AttributeError: # not all objects have __dict__ (e.g. class defines slots) + msg = ( + f"No '__dict__' attribute on {type(instance).__name__!r} " + f"instance to cache {self.attrname!r} property." + ) + raise TypeError(msg) from None + val = cache.get(self.attrname, _NOT_FOUND) + if val is _NOT_FOUND: + with self.lock: + # check if another thread filled cache while we awaited lock + val = cache.get(self.attrname, _NOT_FOUND) + if val is _NOT_FOUND: + val = self.func(instance) + try: + cache[self.attrname] = val + except TypeError: + msg = ( + f"The '__dict__' attribute on {type(instance).__name__!r} instance " + f"does not support item assignment for caching {self.attrname!r} property." + ) + raise TypeError(msg) from None + return val diff --git a/great_ai/large_file/large_file/large_file_base.py b/great_ai/large_file/large_file/large_file_base.py index 9140619..7671503 100644 --- a/great_ai/large_file/large_file/large_file_base.py +++ b/great_ai/large_file/large_file/large_file_base.py @@ -1,5 +1,6 @@ import os import shutil +import sys import tempfile from abc import ABC, abstractmethod from functools import lru_cache @@ -70,7 +71,13 @@ class LargeFileBase(ABC): self._buffering = buffering self._encoding = encoding + + if errors is not None and sys.version_info[1] < 8: + raise RuntimeError( + "The `errors` kwarg is only supported in 3.8 <= Python versions." + ) self._errors = errors + self._newline = newline LargeFileBase.cache_path.mkdir(parents=True, exist_ok=True) @@ -99,16 +106,20 @@ class LargeFileBase(ABC): cls.initialized = True def __enter__(self) -> IO: + params = dict( + mode=self._mode, + buffering=self._buffering, + encoding=self._encoding, + newline=self._newline, + delete=False, + prefix="large_file-", + ) + + if sys.version_info[1] >= 8: + params["errors"] = self._errors + self._file: IO[Any] = ( - tempfile.NamedTemporaryFile( - mode=self._mode, - buffering=self._buffering, - encoding=self._encoding, - newline=self._newline, - errors=self._errors, - delete=False, - prefix="large_file-", - ) + tempfile.NamedTemporaryFile(**params) # type: ignore if "w" in self._mode else open( self.get(), diff --git a/great_ai/large_file/large_file/large_file_mongo.py b/great_ai/large_file/large_file/large_file_mongo.py index 03e9e1e..3cbb9c0 100644 --- a/great_ai/large_file/large_file/large_file_mongo.py +++ b/great_ai/large_file/large_file/large_file_mongo.py @@ -1,5 +1,4 @@ import re -from functools import cached_property from pathlib import Path from typing import Any, List @@ -7,7 +6,7 @@ from gridfs import DEFAULT_CHUNK_SIZE, Database, GridFSBucket from pymongo import MongoClient from ...utilities import get_logger -from ..helper import DownloadProgressBar, UploadProgressBar +from ..helper import DownloadProgressBar, UploadProgressBar, cached_property from ..models import DataInstance from .large_file_base import LargeFileBase diff --git a/great_ai/large_file/large_file/large_file_s3.py b/great_ai/large_file/large_file/large_file_s3.py index 24dd779..eb2a307 100644 --- a/great_ai/large_file/large_file/large_file_s3.py +++ b/great_ai/large_file/large_file/large_file_s3.py @@ -1,11 +1,10 @@ -from functools import cached_property from pathlib import Path from typing import Any, List, Optional import boto3 from ...utilities import get_logger -from ..helper import DownloadProgressBar, UploadProgressBar +from ..helper import DownloadProgressBar, UploadProgressBar, cached_property from ..models import DataInstance from .large_file_base import LargeFileBase diff --git a/tests/test_basic_starters.py b/tests/test_basic_starters.py index d7a8052..1303b22 100644 --- a/tests/test_basic_starters.py +++ b/tests/test_basic_starters.py @@ -25,13 +25,13 @@ def test_create_trivial_cases() -> None: def test_create_with_other_decorator() -> None: @GreatAI.create - @lru_cache + @lru_cache() def hello_world_1(name: str) -> str: return f"Hello {name}!" assert hello_world_1("andras").output == "Hello andras!" - @lru_cache + @lru_cache() @GreatAI.create def hello_world_2(name: str) -> str: return f"Hello {name}!" diff --git a/tests/utilities/test_evaluate_ranking.py b/tests/utilities/test_evaluate_ranking.py index fd29103..8111b6a 100644 --- a/tests/utilities/test_evaluate_ranking.py +++ b/tests/utilities/test_evaluate_ranking.py @@ -77,7 +77,11 @@ def test_empty() -> None: def test_save() -> None: path = Path("test.svg") - path.unlink(missing_ok=True) + try: + path.unlink() + except FileNotFoundError: + # missing_ok only exists >= Python 3.8 + pass evaluate_ranking( ["a", "a", "b", "b", "c", "d", "d", "d"],