mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	chore(ml): added testing and github workflow (#2969)
* added testing * github action for python, made mypy happy * formatted with black * minor fixes and styling * test model cache * cache test dependencies * narrowed model cache tests * moved endpoint tests to their own class * cleaned up fixtures * formatting * removed unused dep
This commit is contained in:
		
							
								
								
									
										30
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -121,6 +121,36 @@ jobs: | ||||
|         working-directory: ./mobile | ||||
|         run: flutter test -j 1 | ||||
|  | ||||
|   ml-unit-tests: | ||||
|     name: Run ML unit tests and checks | ||||
|     runs-on: ubuntu-latest | ||||
|     defaults: | ||||
|       run: | ||||
|         working-directory: ./machine-learning | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Install poetry | ||||
|         run: pipx install poetry | ||||
|       - uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: 3.11 | ||||
|           cache: "poetry" | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           poetry install --with dev | ||||
|       - name: Lint with ruff | ||||
|         run: | | ||||
|           poetry run ruff check --format=github app | ||||
|       - name: Check black formatting | ||||
|         run: | | ||||
|           poetry run black --check app | ||||
|       - name: Run mypy type checking | ||||
|         run: | | ||||
|           poetry run mypy --install-types --non-interactive app/ | ||||
|       - name: Run tests and coverage | ||||
|         run: | | ||||
|           poetry run pytest --cov app | ||||
|  | ||||
|   generated-api-up-to-date: | ||||
|     name: Check generated files are up-to-date | ||||
|     runs-on: ubuntu-latest | ||||
|   | ||||
| @@ -18,6 +18,7 @@ class Settings(BaseSettings): | ||||
|     port: int = 3003 | ||||
|     workers: int = 1 | ||||
|     min_face_score: float = 0.7 | ||||
|     test_full: bool = False | ||||
|  | ||||
|     class Config(BaseSettings.Config): | ||||
|         env_prefix = "MACHINE_LEARNING_" | ||||
|   | ||||
							
								
								
									
										119
									
								
								machine-learning/app/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								machine-learning/app/conftest.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| from types import SimpleNamespace | ||||
| from typing import Any, Iterator, TypeAlias | ||||
| from unittest import mock | ||||
|  | ||||
| import numpy as np | ||||
| import pytest | ||||
| from fastapi.testclient import TestClient | ||||
| from PIL import Image | ||||
|  | ||||
| from .main import app, init_state | ||||
|  | ||||
| ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]] | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def pil_image() -> Image.Image: | ||||
|     return Image.new("RGB", (600, 800)) | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def cv_image(pil_image: Image.Image) -> ndarray: | ||||
|     return np.asarray(pil_image)[:, :, ::-1]  # PIL uses RGB while cv2 uses BGR | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def mock_classifier_pipeline() -> Iterator[mock.Mock]: | ||||
|     with mock.patch("app.models.image_classification.pipeline") as model: | ||||
|         classifier_preds = [ | ||||
|             {"label": "that's an image alright", "score": 0.8}, | ||||
|             {"label": "well it ends with .jpg", "score": 0.1}, | ||||
|             {"label": "idk, im just seeing bytes", "score": 0.05}, | ||||
|             {"label": "not sure", "score": 0.04}, | ||||
|             {"label": "probably a virus", "score": 0.01}, | ||||
|         ] | ||||
|  | ||||
|         def forward( | ||||
|             inputs: Image.Image | list[Image.Image], **kwargs: Any | ||||
|         ) -> list[dict[str, Any]] | list[list[dict[str, Any]]]: | ||||
|             if isinstance(inputs, list) and not all([isinstance(img, Image.Image) for img in inputs]): | ||||
|                 raise TypeError | ||||
|             elif not isinstance(inputs, Image.Image): | ||||
|                 raise TypeError | ||||
|  | ||||
|             if isinstance(inputs, list): | ||||
|                 return [classifier_preds] * len(inputs) | ||||
|  | ||||
|             return classifier_preds | ||||
|  | ||||
|         model.return_value = forward | ||||
|         yield model | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def mock_st() -> Iterator[mock.Mock]: | ||||
|     with mock.patch("app.models.clip.SentenceTransformer") as model: | ||||
|         embedding = np.random.rand(512).astype(np.float32) | ||||
|  | ||||
|         def encode(inputs: Image.Image | list[Image.Image], **kwargs: Any) -> ndarray | list[ndarray]: | ||||
|             #  mypy complains unless isinstance(inputs, list) is used explicitly | ||||
|             img_batch = isinstance(inputs, list) and all([isinstance(inst, Image.Image) for inst in inputs]) | ||||
|             text_batch = isinstance(inputs, list) and all([isinstance(inst, str) for inst in inputs]) | ||||
|             if isinstance(inputs, list) and not any([img_batch, text_batch]): | ||||
|                 raise TypeError | ||||
|  | ||||
|             if isinstance(inputs, list): | ||||
|                 return np.stack([embedding] * len(inputs)) | ||||
|  | ||||
|             return embedding | ||||
|  | ||||
|         mocked = mock.Mock() | ||||
|         mocked.encode = encode | ||||
|         model.return_value = mocked | ||||
|         yield model | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def mock_faceanalysis() -> Iterator[mock.Mock]: | ||||
|     with mock.patch("app.models.facial_recognition.FaceAnalysis") as model: | ||||
|         face_preds = [ | ||||
|             SimpleNamespace(  # this is so these fields can be accessed through dot notation | ||||
|                 **{ | ||||
|                     "bbox": np.random.rand(4).astype(np.float32), | ||||
|                     "kps": np.random.rand(5, 2).astype(np.float32), | ||||
|                     "det_score": np.array([0.67]).astype(np.float32), | ||||
|                     "normed_embedding": np.random.rand(512).astype(np.float32), | ||||
|                 } | ||||
|             ), | ||||
|             SimpleNamespace( | ||||
|                 **{ | ||||
|                     "bbox": np.random.rand(4).astype(np.float32), | ||||
|                     "kps": np.random.rand(5, 2).astype(np.float32), | ||||
|                     "det_score": np.array([0.4]).astype(np.float32), | ||||
|                     "normed_embedding": np.random.rand(512).astype(np.float32), | ||||
|                 } | ||||
|             ), | ||||
|         ] | ||||
|  | ||||
|         def get(image: np.ndarray[int, np.dtype[np.float32]], **kwargs: Any) -> list[SimpleNamespace]: | ||||
|             if not isinstance(image, np.ndarray): | ||||
|                 raise TypeError | ||||
|  | ||||
|             return face_preds | ||||
|  | ||||
|         mocked = mock.Mock() | ||||
|         mocked.get = get | ||||
|         model.return_value = mocked | ||||
|         yield model | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def mock_get_model() -> Iterator[mock.Mock]: | ||||
|     with mock.patch("app.models.cache.InferenceModel.from_model_type", autospec=True) as mocked: | ||||
|         yield mocked | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="session") | ||||
| def deployed_app() -> TestClient: | ||||
|     init_state() | ||||
|     return TestClient(app) | ||||
| @@ -24,9 +24,11 @@ from .schemas import ( | ||||
| app = FastAPI() | ||||
|  | ||||
|  | ||||
| @app.on_event("startup") | ||||
| async def startup_event() -> None: | ||||
| def init_state() -> None: | ||||
|     app.state.model_cache = ModelCache(ttl=settings.model_ttl, revalidate=True) | ||||
|  | ||||
|  | ||||
| async def load_models() -> None: | ||||
|     models = [ | ||||
|         (settings.classification_model, ModelType.IMAGE_CLASSIFICATION), | ||||
|         (settings.clip_image_model, ModelType.CLIP), | ||||
| @@ -42,6 +44,12 @@ async def startup_event() -> None: | ||||
|             InferenceModel.from_model_type(model_type, model_name) | ||||
|  | ||||
|  | ||||
| @app.on_event("startup") | ||||
| async def startup_event() -> None: | ||||
|     init_state() | ||||
|     await load_models() | ||||
|  | ||||
|  | ||||
| def dep_pil_image(byte_image: bytes = Body(...)) -> Image.Image: | ||||
|     return Image.open(BytesIO(byte_image)) | ||||
|  | ||||
| @@ -69,9 +77,7 @@ def ping() -> str: | ||||
| async def image_classification( | ||||
|     image: Image.Image = Depends(dep_pil_image), | ||||
| ) -> list[str]: | ||||
|     model = await app.state.model_cache.get( | ||||
|         settings.classification_model, ModelType.IMAGE_CLASSIFICATION | ||||
|     ) | ||||
|     model = await app.state.model_cache.get(settings.classification_model, ModelType.IMAGE_CLASSIFICATION) | ||||
|     labels = model.predict(image) | ||||
|     return labels | ||||
|  | ||||
| @@ -108,9 +114,7 @@ async def clip_encode_text(payload: TextModelRequest) -> list[float]: | ||||
| async def facial_recognition( | ||||
|     image: cv2.Mat = Depends(dep_cv_image), | ||||
| ) -> list[dict[str, Any]]: | ||||
|     model = await app.state.model_cache.get( | ||||
|         settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION | ||||
|     ) | ||||
|     model = await app.state.model_cache.get(settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION) | ||||
|     faces = model.predict(image) | ||||
|     return faces | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from pathlib import Path | ||||
| from shutil import rmtree | ||||
| from typing import Any | ||||
|  | ||||
| from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf | ||||
| from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf  # type: ignore | ||||
|  | ||||
| from ..config import get_cache_dir | ||||
| from ..schemas import ModelType | ||||
| @@ -14,15 +14,9 @@ from ..schemas import ModelType | ||||
| class InferenceModel(ABC): | ||||
|     _model_type: ModelType | ||||
|  | ||||
|     def __init__( | ||||
|         self, model_name: str, cache_dir: Path | None = None, **model_kwargs | ||||
|     ) -> None: | ||||
|     def __init__(self, model_name: str, cache_dir: Path | str | None = None, **model_kwargs: Any) -> None: | ||||
|         self.model_name = model_name | ||||
|         self._cache_dir = ( | ||||
|             cache_dir | ||||
|             if cache_dir is not None | ||||
|             else get_cache_dir(model_name, self.model_type) | ||||
|         ) | ||||
|         self._cache_dir = Path(cache_dir) if cache_dir is not None else get_cache_dir(model_name, self.model_type) | ||||
|  | ||||
|         try: | ||||
|             self.load(**model_kwargs) | ||||
| @@ -51,12 +45,8 @@ class InferenceModel(ABC): | ||||
|         self._cache_dir = cache_dir | ||||
|  | ||||
|     @classmethod | ||||
|     def from_model_type( | ||||
|         cls, model_type: ModelType, model_name, **model_kwargs | ||||
|     ) -> InferenceModel: | ||||
|         subclasses = { | ||||
|             subclass._model_type: subclass for subclass in cls.__subclasses__() | ||||
|         } | ||||
|     def from_model_type(cls, model_type: ModelType, model_name: str, **model_kwargs: Any) -> InferenceModel: | ||||
|         subclasses = {subclass._model_type: subclass for subclass in cls.__subclasses__()} | ||||
|         if model_type not in subclasses: | ||||
|             raise ValueError(f"Unsupported model type: {model_type}") | ||||
|  | ||||
| @@ -66,8 +56,6 @@ class InferenceModel(ABC): | ||||
|         if not self.cache_dir.exists(): | ||||
|             return | ||||
|         elif not rmtree.avoids_symlink_attacks: | ||||
|             raise RuntimeError( | ||||
|                 "Attempted to clear cache, but rmtree is not safe on this platform." | ||||
|             ) | ||||
|             raise RuntimeError("Attempted to clear cache, but rmtree is not safe on this platform.") | ||||
|  | ||||
|         rmtree(self.cache_dir) | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import asyncio | ||||
| from typing import Any | ||||
|  | ||||
| from aiocache.backends.memory import SimpleMemoryCache | ||||
| from aiocache.lock import OptimisticLock | ||||
| @@ -34,13 +35,9 @@ class ModelCache: | ||||
|         if profiling: | ||||
|             plugins.append(TimingPlugin()) | ||||
|  | ||||
|         self.cache = SimpleMemoryCache( | ||||
|             ttl=ttl, timeout=timeout, plugins=plugins, namespace=None | ||||
|         ) | ||||
|         self.cache = SimpleMemoryCache(ttl=ttl, timeout=timeout, plugins=plugins, namespace=None) | ||||
|  | ||||
|     async def get( | ||||
|         self, model_name: str, model_type: ModelType, **model_kwargs | ||||
|     ) -> InferenceModel: | ||||
|     async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel: | ||||
|         """ | ||||
|         Args: | ||||
|             model_name: Name of model in the model hub used for the task. | ||||
| @@ -56,9 +53,7 @@ class ModelCache: | ||||
|             async with OptimisticLock(self.cache, key) as lock: | ||||
|                 model = await asyncio.get_running_loop().run_in_executor( | ||||
|                     None, | ||||
|                     lambda: InferenceModel.from_model_type( | ||||
|                         model_type, model_name, **model_kwargs | ||||
|                     ), | ||||
|                     lambda: InferenceModel.from_model_type(model_type, model_name, **model_kwargs), | ||||
|                 ) | ||||
|                 await lock.cas(model, ttl=self.ttl) | ||||
|         return model | ||||
| @@ -73,7 +68,14 @@ class ModelCache: | ||||
| class RevalidationPlugin(BasePlugin): | ||||
|     """Revalidates cache item's TTL after cache hit.""" | ||||
|  | ||||
|     async def post_get(self, client, key, ret=None, namespace=None, **kwargs): | ||||
|     async def post_get( | ||||
|         self, | ||||
|         client: SimpleMemoryCache, | ||||
|         key: str, | ||||
|         ret: Any | None = None, | ||||
|         namespace: str | None = None, | ||||
|         **kwargs: Any, | ||||
|     ) -> None: | ||||
|         if ret is None: | ||||
|             return | ||||
|         if namespace is not None: | ||||
| @@ -81,7 +83,14 @@ class RevalidationPlugin(BasePlugin): | ||||
|         if key in client._handlers: | ||||
|             await client.expire(key, client.ttl) | ||||
|  | ||||
|     async def post_multi_get(self, client, keys, ret=None, namespace=None, **kwargs): | ||||
|     async def post_multi_get( | ||||
|         self, | ||||
|         client: SimpleMemoryCache, | ||||
|         keys: list[str], | ||||
|         ret: list[Any] | None = None, | ||||
|         namespace: str | None = None, | ||||
|         **kwargs: Any, | ||||
|     ) -> None: | ||||
|         if ret is None: | ||||
|             return | ||||
|  | ||||
|   | ||||
| @@ -16,8 +16,8 @@ class FaceRecognizer(InferenceModel): | ||||
|         self, | ||||
|         model_name: str, | ||||
|         min_score: float = settings.min_face_score, | ||||
|         cache_dir: Path | None = None, | ||||
|         **model_kwargs, | ||||
|         cache_dir: Path | str | None = None, | ||||
|         **model_kwargs: Any, | ||||
|     ) -> None: | ||||
|         self.min_score = min_score | ||||
|         super().__init__(model_name, cache_dir, **model_kwargs) | ||||
|   | ||||
| @@ -16,8 +16,8 @@ class ImageClassifier(InferenceModel): | ||||
|         self, | ||||
|         model_name: str, | ||||
|         min_score: float = settings.min_tag_score, | ||||
|         cache_dir: Path | None = None, | ||||
|         **model_kwargs, | ||||
|         cache_dir: Path | str | None = None, | ||||
|         **model_kwargs: Any, | ||||
|     ) -> None: | ||||
|         self.min_score = min_score | ||||
|         super().__init__(model_name, cache_dir, **model_kwargs) | ||||
| @@ -30,13 +30,7 @@ class ImageClassifier(InferenceModel): | ||||
|         ) | ||||
|  | ||||
|     def predict(self, image: Image) -> list[str]: | ||||
|         predictions = self.model(image) | ||||
|         tags = list( | ||||
|             { | ||||
|                 tag | ||||
|                 for pred in predictions | ||||
|                 for tag in pred["label"].split(", ") | ||||
|                 if pred["score"] >= self.min_score | ||||
|             } | ||||
|         ) | ||||
|         predictions: list[dict[str, Any]] = self.model(image)  # type: ignore | ||||
|         tags = [tag for pred in predictions for tag in pred["label"].split(", ") if pred["score"] >= self.min_score] | ||||
|  | ||||
|         return tags | ||||
|   | ||||
| @@ -4,10 +4,7 @@ from pydantic import BaseModel | ||||
|  | ||||
|  | ||||
| def to_lower_camel(string: str) -> str: | ||||
|     tokens = [ | ||||
|         token.capitalize() if i > 0 else token | ||||
|         for i, token in enumerate(string.split("_")) | ||||
|     ] | ||||
|     tokens = [token.capitalize() if i > 0 else token for i, token in enumerate(string.split("_"))] | ||||
|     return "".join(tokens) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										183
									
								
								machine-learning/app/test_main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								machine-learning/app/test_main.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,183 @@ | ||||
| from io import BytesIO | ||||
| from pathlib import Path | ||||
| from unittest import mock | ||||
|  | ||||
| import cv2 | ||||
| import pytest | ||||
| from fastapi.testclient import TestClient | ||||
| from PIL import Image | ||||
|  | ||||
| from .config import settings | ||||
| from .models.cache import ModelCache | ||||
| from .models.clip import CLIPSTEncoder | ||||
| from .models.facial_recognition import FaceRecognizer | ||||
| from .models.image_classification import ImageClassifier | ||||
| from .schemas import ModelType | ||||
|  | ||||
|  | ||||
| class TestImageClassifier: | ||||
|     def test_init(self, mock_classifier_pipeline: mock.Mock) -> None: | ||||
|         cache_dir = Path("test_cache") | ||||
|         classifier = ImageClassifier("test_model_name", 0.5, cache_dir=cache_dir) | ||||
|  | ||||
|         assert classifier.min_score == 0.5 | ||||
|         mock_classifier_pipeline.assert_called_once_with( | ||||
|             "image-classification", | ||||
|             "test_model_name", | ||||
|             model_kwargs={"cache_dir": cache_dir}, | ||||
|         ) | ||||
|  | ||||
|     def test_min_score(self, pil_image: Image.Image, mock_classifier_pipeline: mock.Mock) -> None: | ||||
|         classifier = ImageClassifier("test_model_name", min_score=0.0) | ||||
|         classifier.min_score = 0.0 | ||||
|         all_labels = classifier.predict(pil_image) | ||||
|         classifier.min_score = 0.5 | ||||
|         filtered_labels = classifier.predict(pil_image) | ||||
|  | ||||
|         assert all_labels == [ | ||||
|             "that's an image alright", | ||||
|             "well it ends with .jpg", | ||||
|             "idk", | ||||
|             "im just seeing bytes", | ||||
|             "not sure", | ||||
|             "probably a virus", | ||||
|         ] | ||||
|         assert filtered_labels == ["that's an image alright"] | ||||
|  | ||||
|  | ||||
| class TestCLIP: | ||||
|     def test_init(self, mock_st: mock.Mock) -> None: | ||||
|         CLIPSTEncoder("test_model_name", cache_dir="test_cache") | ||||
|  | ||||
|         mock_st.assert_called_once_with("test_model_name", cache_folder="test_cache") | ||||
|  | ||||
|     def test_basic_image(self, pil_image: Image.Image, mock_st: mock.Mock) -> None: | ||||
|         clip_encoder = CLIPSTEncoder("test_model_name", cache_dir="test_cache") | ||||
|         embedding = clip_encoder.predict(pil_image) | ||||
|  | ||||
|         assert isinstance(embedding, list) | ||||
|         assert len(embedding) == 512 | ||||
|         assert all([isinstance(num, float) for num in embedding]) | ||||
|         mock_st.assert_called_once() | ||||
|  | ||||
|     def test_basic_text(self, mock_st: mock.Mock) -> None: | ||||
|         clip_encoder = CLIPSTEncoder("test_model_name", cache_dir="test_cache") | ||||
|         embedding = clip_encoder.predict("test search query") | ||||
|  | ||||
|         assert isinstance(embedding, list) | ||||
|         assert len(embedding) == 512 | ||||
|         assert all([isinstance(num, float) for num in embedding]) | ||||
|         mock_st.assert_called_once() | ||||
|  | ||||
|  | ||||
| class TestFaceRecognition: | ||||
|     def test_init(self, mock_faceanalysis: mock.Mock) -> None: | ||||
|         FaceRecognizer("test_model_name", cache_dir="test_cache") | ||||
|  | ||||
|         mock_faceanalysis.assert_called_once_with( | ||||
|             name="test_model_name", | ||||
|             root="test_cache", | ||||
|             allowed_modules=["detection", "recognition"], | ||||
|         ) | ||||
|  | ||||
|     def test_basic(self, cv_image: cv2.Mat, mock_faceanalysis: mock.Mock) -> None: | ||||
|         face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache") | ||||
|         faces = face_recognizer.predict(cv_image) | ||||
|  | ||||
|         assert len(faces) == 2 | ||||
|         for face in faces: | ||||
|             assert face["imageHeight"] == 800 | ||||
|             assert face["imageWidth"] == 600 | ||||
|             assert isinstance(face["embedding"], list) | ||||
|             assert len(face["embedding"]) == 512 | ||||
|             assert all([isinstance(num, float) for num in face["embedding"]]) | ||||
|  | ||||
|         mock_faceanalysis.assert_called_once() | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| class TestCache: | ||||
|     async def test_caches(self, mock_get_model: mock.Mock) -> None: | ||||
|         model_cache = ModelCache() | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION) | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION) | ||||
|         assert len(model_cache.cache._cache) == 1 | ||||
|         mock_get_model.assert_called_once() | ||||
|  | ||||
|     async def test_kwargs_used(self, mock_get_model: mock.Mock) -> None: | ||||
|         model_cache = ModelCache() | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION, cache_dir="test_cache") | ||||
|         mock_get_model.assert_called_once_with( | ||||
|             ModelType.IMAGE_CLASSIFICATION, "test_model_name", cache_dir="test_cache" | ||||
|         ) | ||||
|  | ||||
|     async def test_different_clip(self, mock_get_model: mock.Mock) -> None: | ||||
|         model_cache = ModelCache() | ||||
|         await model_cache.get("test_image_model_name", ModelType.CLIP) | ||||
|         await model_cache.get("test_text_model_name", ModelType.CLIP) | ||||
|         mock_get_model.assert_has_calls( | ||||
|             [ | ||||
|                 mock.call(ModelType.CLIP, "test_image_model_name"), | ||||
|                 mock.call(ModelType.CLIP, "test_text_model_name"), | ||||
|             ] | ||||
|         ) | ||||
|         assert len(model_cache.cache._cache) == 2 | ||||
|  | ||||
|     @mock.patch("app.models.cache.OptimisticLock", autospec=True) | ||||
|     async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None: | ||||
|         model_cache = ModelCache(ttl=100) | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION) | ||||
|         mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100) | ||||
|  | ||||
|     @mock.patch("app.models.cache.SimpleMemoryCache.expire") | ||||
|     async def test_revalidate(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None: | ||||
|         model_cache = ModelCache(ttl=100, revalidate=True) | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION) | ||||
|         await model_cache.get("test_model_name", ModelType.IMAGE_CLASSIFICATION) | ||||
|         mock_cache_expire.assert_called_once_with(mock.ANY, 100) | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     not settings.test_full, | ||||
|     reason="More time-consuming since it deploys the app and loads models.", | ||||
| ) | ||||
| class TestEndpoints: | ||||
|     def test_tagging_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None: | ||||
|         byte_image = BytesIO() | ||||
|         pil_image.save(byte_image, format="jpeg") | ||||
|         headers = {"Content-Type": "image/jpg"} | ||||
|         response = deployed_app.post( | ||||
|             "http://localhost:3003/image-classifier/tag-image", | ||||
|             content=byte_image.getvalue(), | ||||
|             headers=headers, | ||||
|         ) | ||||
|         assert response.status_code == 200 | ||||
|  | ||||
|     def test_clip_image_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None: | ||||
|         byte_image = BytesIO() | ||||
|         pil_image.save(byte_image, format="jpeg") | ||||
|         headers = {"Content-Type": "image/jpg"} | ||||
|         response = deployed_app.post( | ||||
|             "http://localhost:3003/sentence-transformer/encode-image", | ||||
|             content=byte_image.getvalue(), | ||||
|             headers=headers, | ||||
|         ) | ||||
|         assert response.status_code == 200 | ||||
|  | ||||
|     def test_clip_text_endpoint(self, deployed_app: TestClient) -> None: | ||||
|         response = deployed_app.post( | ||||
|             "http://localhost:3003/sentence-transformer/encode-text", | ||||
|             json={"text": "test search query"}, | ||||
|         ) | ||||
|         assert response.status_code == 200 | ||||
|  | ||||
|     def test_face_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None: | ||||
|         byte_image = BytesIO() | ||||
|         pil_image.save(byte_image, format="jpeg") | ||||
|         headers = {"Content-Type": "image/jpg"} | ||||
|         response = deployed_app.post( | ||||
|             "http://localhost:3003/facial-recognition/detect-faces", | ||||
|             content=byte_image.getvalue(), | ||||
|             headers=headers, | ||||
|         ) | ||||
|         assert response.status_code == 200 | ||||
							
								
								
									
										249
									
								
								machine-learning/poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										249
									
								
								machine-learning/poetry.lock
									
									
									
										generated
									
									
									
								
							| @@ -424,13 +424,13 @@ cron = ["capturer (>=2.4)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "configargparse" | ||||
| version = "1.5.3" | ||||
| version = "1.5.5" | ||||
| description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables." | ||||
| optional = false | ||||
| python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | ||||
| files = [ | ||||
|     {file = "ConfigArgParse-1.5.3-py3-none-any.whl", hash = "sha256:18f6535a2db9f6e02bd5626cc7455eac3e96b9ab3d969d366f9aafd5c5c00fe7"}, | ||||
|     {file = "ConfigArgParse-1.5.3.tar.gz", hash = "sha256:1b0b3cbf664ab59dada57123c81eff3d9737e0d11d8cf79e3d6eb10823f1739f"}, | ||||
|     {file = "ConfigArgParse-1.5.5-py3-none-any.whl", hash = "sha256:541360ddc1b15c517f95c0d02d1fca4591266628f3667acdc5d13dccc78884ca"}, | ||||
|     {file = "ConfigArgParse-1.5.5.tar.gz", hash = "sha256:363d80a6d35614bd446e2f2b1b216f3b33741d03ac6d0a92803306f40e555b58"}, | ||||
| ] | ||||
|  | ||||
| [package.extras] | ||||
| @@ -495,6 +495,78 @@ mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pill | ||||
| test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] | ||||
| test-no-images = ["pytest", "pytest-cov", "wurlitzer"] | ||||
|  | ||||
| [[package]] | ||||
| name = "coverage" | ||||
| version = "7.2.7" | ||||
| description = "Code coverage measurement for Python" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, | ||||
|     {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, | ||||
|     {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, | ||||
|     {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, | ||||
|     {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, | ||||
|     {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, | ||||
|     {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, | ||||
|     {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, | ||||
|     {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, | ||||
| ] | ||||
|  | ||||
| [package.extras] | ||||
| toml = ["tomli"] | ||||
|  | ||||
| [[package]] | ||||
| name = "cycler" | ||||
| version = "0.11.0" | ||||
| @@ -639,18 +711,17 @@ Flask = "*" | ||||
|  | ||||
| [[package]] | ||||
| name = "flask-cors" | ||||
| version = "3.0.10" | ||||
| version = "4.0.0" | ||||
| description = "A Flask extension adding a decorator for CORS support" | ||||
| optional = false | ||||
| python-versions = "*" | ||||
| files = [ | ||||
|     {file = "Flask-Cors-3.0.10.tar.gz", hash = "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"}, | ||||
|     {file = "Flask_Cors-3.0.10-py2.py3-none-any.whl", hash = "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438"}, | ||||
|     {file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"}, | ||||
|     {file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| Flask = ">=0.9" | ||||
| Six = "*" | ||||
|  | ||||
| [[package]] | ||||
| name = "flatbuffers" | ||||
| @@ -1039,6 +1110,27 @@ files = [ | ||||
|     {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "httpcore" | ||||
| version = "0.17.2" | ||||
| description = "A minimal low-level HTTP client." | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "httpcore-0.17.2-py3-none-any.whl", hash = "sha256:5581b9c12379c4288fe70f43c710d16060c10080617001e6b22a3b6dbcbefd36"}, | ||||
|     {file = "httpcore-0.17.2.tar.gz", hash = "sha256:125f8375ab60036db632f34f4b627a9ad085048eef7cb7d2616fea0f739f98af"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| anyio = ">=3.0,<5.0" | ||||
| certifi = "*" | ||||
| h11 = ">=0.13,<0.15" | ||||
| sniffio = "==1.*" | ||||
|  | ||||
| [package.extras] | ||||
| http2 = ["h2 (>=3,<5)"] | ||||
| socks = ["socksio (==1.*)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "httptools" | ||||
| version = "0.5.0" | ||||
| @@ -1092,6 +1184,29 @@ files = [ | ||||
| [package.extras] | ||||
| test = ["Cython (>=0.29.24,<0.30.0)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "httpx" | ||||
| version = "0.24.1" | ||||
| description = "The next generation HTTP client." | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, | ||||
|     {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| certifi = "*" | ||||
| httpcore = ">=0.15.0,<0.18.0" | ||||
| idna = "*" | ||||
| sniffio = "*" | ||||
|  | ||||
| [package.extras] | ||||
| brotli = ["brotli", "brotlicffi"] | ||||
| cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] | ||||
| http2 = ["h2 (>=3,<5)"] | ||||
| socks = ["socksio (==1.*)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "huggingface-hub" | ||||
| version = "0.15.1" | ||||
| @@ -1584,42 +1699,42 @@ files = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "mypy" | ||||
| version = "1.4.0" | ||||
| version = "1.4.1" | ||||
| description = "Optional static typing for Python" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "mypy-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3af348e0925a59213244f28c7c0c3a2c2088b4ba2fe9d6c8d4fbb0aba0b7d05"}, | ||||
|     {file = "mypy-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0b2e0da7ff9dd8d2066d093d35a169305fc4e38db378281fce096768a3dbdbf"}, | ||||
|     {file = "mypy-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210fe0f39ec5be45dd9d0de253cb79245f0a6f27631d62e0c9c7988be7152965"}, | ||||
|     {file = "mypy-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f7a5971490fd4a5a436e143105a1f78fa8b3fe95b30fff2a77542b4f3227a01f"}, | ||||
|     {file = "mypy-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:50f65f0e9985f1e50040e603baebab83efed9eb37e15a22a4246fa7cd660f981"}, | ||||
|     {file = "mypy-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1b5c875fcf3e7217a3de7f708166f641ca154b589664c44a6fd6d9f17d9e7e"}, | ||||
|     {file = "mypy-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4c734d947e761c7ceb1f09a98359dd5666460acbc39f7d0a6b6beec373c5840"}, | ||||
|     {file = "mypy-1.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5984a8d13d35624e3b235a793c814433d810acba9eeefe665cdfed3d08bc3af"}, | ||||
|     {file = "mypy-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0f98973e39e4a98709546a9afd82e1ffcc50c6ec9ce6f7870f33ebbf0bd4f26d"}, | ||||
|     {file = "mypy-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:19d42b08c7532d736a7e0fb29525855e355fa51fd6aef4f9bbc80749ff64b1a2"}, | ||||
|     {file = "mypy-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ba9a69172abaa73910643744d3848877d6aac4a20c41742027dcfd8d78f05d9"}, | ||||
|     {file = "mypy-1.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a34eed094c16cad0f6b0d889811592c7a9b7acf10d10a7356349e325d8704b4f"}, | ||||
|     {file = "mypy-1.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:53c2a1fed81e05ded10a4557fe12bae05b9ecf9153f162c662a71d924d504135"}, | ||||
|     {file = "mypy-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bba57b4d2328740749f676807fcf3036e9de723530781405cc5a5e41fc6e20de"}, | ||||
|     {file = "mypy-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:653863c75f0dbb687d92eb0d4bd9fe7047d096987ecac93bb7b1bc336de48ebd"}, | ||||
|     {file = "mypy-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7461469e163f87a087a5e7aa224102a30f037c11a096a0ceeb721cb0dce274c8"}, | ||||
|     {file = "mypy-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf0ca95e4b8adeaf07815a78b4096b65adf64ea7871b39a2116c19497fcd0dd"}, | ||||
|     {file = "mypy-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94a81b9354545123feb1a99b960faeff9e1fa204fce47e0042335b473d71530d"}, | ||||
|     {file = "mypy-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:67242d5b28ed0fa88edd8f880aed24da481929467fdbca6487167cb5e3fd31ff"}, | ||||
|     {file = "mypy-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3f2b353eebef669529d9bd5ae3566905a685ae98b3af3aad7476d0d519714758"}, | ||||
|     {file = "mypy-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62bf18d97c6b089f77f0067b4e321db089d8520cdeefc6ae3ec0f873621c22e5"}, | ||||
|     {file = "mypy-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca33ab70a4aaa75bb01086a0b04f0ba8441e51e06fc57e28585176b08cad533b"}, | ||||
|     {file = "mypy-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5a0ee54c2cb0f957f8a6f41794d68f1a7e32b9968675ade5846f538504856d42"}, | ||||
|     {file = "mypy-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6c34d43e3d54ad05024576aef28081d9d0580f6fa7f131255f54020eb12f5352"}, | ||||
|     {file = "mypy-1.4.0-py3-none-any.whl", hash = "sha256:f051ca656be0c179c735a4c3193f307d34c92fdc4908d44fd4516fbe8b10567d"}, | ||||
|     {file = "mypy-1.4.0.tar.gz", hash = "sha256:de1e7e68148a213036276d1f5303b3836ad9a774188961eb2684eddff593b042"}, | ||||
|     {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, | ||||
|     {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, | ||||
|     {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, | ||||
|     {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, | ||||
|     {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, | ||||
|     {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, | ||||
|     {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, | ||||
|     {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, | ||||
|     {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, | ||||
|     {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, | ||||
|     {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, | ||||
|     {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, | ||||
|     {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, | ||||
|     {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, | ||||
|     {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, | ||||
|     {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, | ||||
|     {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, | ||||
|     {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, | ||||
|     {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, | ||||
|     {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, | ||||
|     {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, | ||||
|     {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, | ||||
|     {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, | ||||
|     {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, | ||||
|     {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, | ||||
|     {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| mypy-extensions = ">=1.0.0" | ||||
| typing-extensions = ">=3.10" | ||||
| typing-extensions = ">=4.1.0" | ||||
|  | ||||
| [package.extras] | ||||
| dmypy = ["psutil (>=4.0)"] | ||||
| @@ -2133,6 +2248,42 @@ pluggy = ">=0.12,<2.0" | ||||
| [package.extras] | ||||
| testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] | ||||
|  | ||||
| [[package]] | ||||
| name = "pytest-asyncio" | ||||
| version = "0.21.0" | ||||
| description = "Pytest support for asyncio" | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "pytest-asyncio-0.21.0.tar.gz", hash = "sha256:2b38a496aef56f56b0e87557ec313e11e1ab9276fc3863f6a7be0f1d0e415e1b"}, | ||||
|     {file = "pytest_asyncio-0.21.0-py3-none-any.whl", hash = "sha256:f2b3366b7cd501a4056858bd39349d5af19742aed2d81660b7998b6341c7eb9c"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| pytest = ">=7.0.0" | ||||
|  | ||||
| [package.extras] | ||||
| docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] | ||||
| testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "pytest-cov" | ||||
| version = "4.1.0" | ||||
| description = "Pytest plugin for measuring coverage." | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, | ||||
|     {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| coverage = {version = ">=5.2.1", extras = ["toml"]} | ||||
| pytest = ">=4.6" | ||||
|  | ||||
| [package.extras] | ||||
| testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] | ||||
|  | ||||
| [[package]] | ||||
| name = "python-dateutil" | ||||
| version = "2.8.2" | ||||
| @@ -2504,6 +2655,32 @@ files = [ | ||||
|     {file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"}, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ruff" | ||||
| version = "0.0.272" | ||||
| description = "An extremely fast Python linter, written in Rust." | ||||
| optional = false | ||||
| python-versions = ">=3.7" | ||||
| files = [ | ||||
|     {file = "ruff-0.0.272-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:ae9b57546e118660175d45d264b87e9b4c19405c75b587b6e4d21e6a17bf4fdf"}, | ||||
|     {file = "ruff-0.0.272-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:1609b864a8d7ee75a8c07578bdea0a7db75a144404e75ef3162e0042bfdc100d"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee76b4f05fcfff37bd6ac209d1370520d509ea70b5a637bdf0a04d0c99e13dff"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48eccf225615e106341a641f826b15224b8a4240b84269ead62f0afd6d7e2d95"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:677284430ac539bb23421a2b431b4ebc588097ef3ef918d0e0a8d8ed31fea216"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9c4bfb75456a8e1efe14c52fcefb89cfb8f2a0d31ed8d804b82c6cf2dc29c42c"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86bc788245361a8148ff98667da938a01e1606b28a45e50ac977b09d3ad2c538"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b2ea68d2aa69fff1b20b67636b1e3e22a6a39e476c880da1282c3e4bf6ee5a"}, | ||||
|     {file = "ruff-0.0.272-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd2bbe337a3f84958f796c77820d55ac2db1e6753f39d1d1baed44e07f13f96d"}, | ||||
|     {file = "ruff-0.0.272-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d5a208f8ef0e51d4746930589f54f9f92f84bb69a7d15b1de34ce80a7681bc00"}, | ||||
|     {file = "ruff-0.0.272-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:905ff8f3d6206ad56fcd70674453527b9011c8b0dc73ead27618426feff6908e"}, | ||||
|     {file = "ruff-0.0.272-py3-none-musllinux_1_2_i686.whl", hash = "sha256:19643d448f76b1eb8a764719072e9c885968971bfba872e14e7257e08bc2f2b7"}, | ||||
|     {file = "ruff-0.0.272-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:691d72a00a99707a4e0b2846690961157aef7b17b6b884f6b4420a9f25cd39b5"}, | ||||
|     {file = "ruff-0.0.272-py3-none-win32.whl", hash = "sha256:dc406e5d756d932da95f3af082814d2467943631a587339ee65e5a4f4fbe83eb"}, | ||||
|     {file = "ruff-0.0.272-py3-none-win_amd64.whl", hash = "sha256:a37ec80e238ead2969b746d7d1b6b0d31aa799498e9ba4281ab505b93e1f4b28"}, | ||||
|     {file = "ruff-0.0.272-py3-none-win_arm64.whl", hash = "sha256:06b8ee4eb8711ab119db51028dd9f5384b44728c23586424fd6e241a5b9c4a3b"}, | ||||
|     {file = "ruff-0.0.272.tar.gz", hash = "sha256:273a01dc8c3c4fd4c2af7ea7a67c8d39bb09bce466e640dd170034da75d14cab"}, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "safetensors" | ||||
| version = "0.3.1" | ||||
| @@ -3425,4 +3602,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] | ||||
| [metadata] | ||||
| lock-version = "2.0" | ||||
| python-versions = "^3.11" | ||||
| content-hash = "2981003c319d9990f05abec1e3d02dc1ea6680b0bf1590376c5e47801311d89f" | ||||
| content-hash = "e0ac37404f0c11ee5b478d2c7113986a2d40d02e2b985ff18846374a65025a26" | ||||
|   | ||||
| @@ -22,6 +22,8 @@ fastapi = "^0.95.2" | ||||
| uvicorn = {extras = ["standard"], version = "^0.22.0"} | ||||
| pydantic = "^1.10.8" | ||||
| aiocache = "^0.12.1" | ||||
| pytest-cov = "^4.1.0" | ||||
| ruff = "^0.0.272" | ||||
|  | ||||
| [tool.poetry.group.dev.dependencies] | ||||
| mypy = "^1.3.0" | ||||
| @@ -29,6 +31,8 @@ black = "^23.3.0" | ||||
| pytest = "^7.3.1" | ||||
| locust = "^2.15.1" | ||||
| gunicorn = "^20.1.0" | ||||
| httpx = "^0.24.1" | ||||
| pytest-asyncio = "^0.21.0" | ||||
|  | ||||
| [[tool.poetry.source]] | ||||
| name = "pytorch-cpu" | ||||
| @@ -39,9 +43,6 @@ priority = "explicit" | ||||
| requires = ["poetry-core"] | ||||
| build-backend = "poetry.core.masonry.api" | ||||
|  | ||||
| [tool.flake8] | ||||
| max-line-length = 120 | ||||
|  | ||||
| [tool.mypy] | ||||
| python_version = "3.11" | ||||
| plugins = "pydantic.mypy" | ||||
| @@ -49,7 +50,6 @@ follow_imports = "silent" | ||||
| warn_redundant_casts = true | ||||
| disallow_any_generics = true | ||||
| check_untyped_defs = true | ||||
| no_implicit_reexport = true | ||||
| disallow_untyped_defs = true | ||||
|  | ||||
| [tool.pydantic-mypy] | ||||
| @@ -57,3 +57,28 @@ init_forbid_extra = true | ||||
| init_typed = true | ||||
| warn_required_dynamic_aliases = true | ||||
| warn_untyped_fields = true | ||||
|  | ||||
| [[tool.mypy.overrides]] | ||||
| module = [ | ||||
|     "transformers.pipelines", | ||||
|     "cv2", | ||||
|     "insightface.app", | ||||
|     "sentence_transformers", | ||||
|     "aiocache.backends.memory", | ||||
|     "aiocache.lock", | ||||
|     "aiocache.plugins" | ||||
| ] | ||||
| ignore_missing_imports = true | ||||
|  | ||||
| [tool.ruff] | ||||
| line-length = 120 | ||||
| target-version = "py311" | ||||
| select = ["E", "F", "I"] | ||||
| ignore = ["F401"] | ||||
|  | ||||
| [tool.ruff.per-file-ignores] | ||||
| "test_main.py" = ["F403"] | ||||
|  | ||||
| [tool.black] | ||||
| line-length = 120 | ||||
| target-version = ['py311'] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user