From bc9856686fc4a29d42d1baab693483dd28a6f7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislas=20Bruhi=C3=A8re?= Date: Sat, 24 Jun 2023 15:34:25 +0200 Subject: [PATCH 01/22] feat: remove s3 file from annotations and use an observation array instead --- src/app/api/routes/annotations.py | 74 ++----------------------- src/app/api/schemas.py | 9 +--- src/app/db/models.py | 6 +-- src/tests/routes/test_annotations.py | 81 +++------------------------- 4 files changed, 13 insertions(+), 157 deletions(-) diff --git a/src/app/api/routes/annotations.py b/src/app/api/routes/annotations.py index 4d3158a..66dff63 100644 --- a/src/app/api/routes/annotations.py +++ b/src/app/api/routes/annotations.py @@ -5,15 +5,13 @@ from typing import Any, Dict, List -from fastapi import APIRouter, BackgroundTasks, File, HTTPException, Path, Security, UploadFile, status +from fastapi import APIRouter, Path, Security, status from app.api import crud from app.api.crud.authorizations import check_access_read, is_admin_access from app.api.deps import get_current_access -from app.api.schemas import AccessType, AnnotationCreation, AnnotationIn, AnnotationOut, AnnotationUrl -from app.api.security import hash_content_file +from app.api.schemas import AccessType, AnnotationIn, AnnotationOut from app.db import annotations -from app.services import resolve_bucket_key, s3_bucket router = APIRouter() @@ -33,7 +31,7 @@ async def create_annotation( payload: AnnotationIn, _=Security(get_current_access, scopes=[AccessType.admin, AccessType.user]) ): """ - Creates an annotation related to specific media, based on media_id as argument + Creates an annotation related to specific media, based on media_id as argument, and with as many observations as needed Below, click on "Schema" for more detailed information about arguments or "Example Value" to get a concrete idea of arguments @@ -86,69 +84,3 @@ async def delete_annotation( Based on a annotation_id, deletes the specified annotation """ return await crud.delete_entry(annotations, annotation_id) - - -@router.post("/{annotation_id}/upload", response_model=AnnotationOut, status_code=200) -async def upload_annotation( - background_tasks: BackgroundTasks, - annotation_id: int = Path(..., gt=0), - file: UploadFile = File(...), -): - """ - Upload a annotation (image or video) linked to an existing annotation object in the DB - """ - - # Check in DB - entry = await check_annotation_registration(annotation_id) - - # Concatenate the first 32 chars (to avoid system interactions issues) of SHA256 hash with file extension - file_hash = hash_content_file(file.file.read()) - file_name = f"{file_hash[:32]}.{file.filename.rpartition('.')[-1]}" - # Reset byte position of the file (cf. https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile) - await file.seek(0) - # Use MD5 to verify upload - md5_hash = hash_content_file(file.file.read(), use_md5=True) - await file.seek(0) - # If files are in a subfolder of the bucket, prepend the folder path - bucket_key = resolve_bucket_key(file_name, "annotations") - - # Upload if bucket_key is different (otherwise the content is the exact same) - if isinstance(entry["bucket_key"], str) and entry["bucket_key"] == bucket_key: - return await crud.get_entry(annotations, annotation_id) - else: - # Failed upload - if not await s3_bucket.upload_file(bucket_key=bucket_key, file_binary=file.file): - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed upload") - # Data integrity check - file_meta = await s3_bucket.get_file_metadata(bucket_key) - # Corrupted file - if md5_hash != file_meta["ETag"].replace('"', ""): - # Delete the corrupted upload - await s3_bucket.delete_file(bucket_key) - # Raise the exception - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="Data was corrupted during upload", - ) - # If a file was previously uploaded, delete it - if isinstance(entry["bucket_key"], str): - await s3_bucket.delete_file(entry["bucket_key"]) - - entry_dict = dict(**entry) - entry_dict["bucket_key"] = bucket_key - return await crud.update_entry(annotations, AnnotationCreation(**entry_dict), annotation_id) - - -@router.get("/{annotation_id}/url", response_model=AnnotationUrl, status_code=200) -async def get_annotation_url( - annotation_id: int = Path(..., gt=0), - requester=Security(get_current_access, scopes=[AccessType.admin, AccessType.user]), -): - """Resolve the temporary media image URL""" - await check_access_read(requester.id) - - # Check in DB - annotation_instance = await check_annotation_registration(annotation_id) - # Check in bucket - temp_public_url = await s3_bucket.get_public_url(annotation_instance["bucket_key"]) - return AnnotationUrl(url=temp_public_url) diff --git a/src/app/api/schemas.py b/src/app/api/schemas.py index fb79c23..a1e511c 100644 --- a/src/app/api/schemas.py +++ b/src/app/api/schemas.py @@ -88,15 +88,8 @@ class MediaUrl(BaseModel): # Annotation class AnnotationIn(BaseModel): media_id: int = Field(..., gt=0) - - -class AnnotationCreation(AnnotationIn): - bucket_key: str = Field(...) + observations: List[str] = Field(..., min_items=0) class AnnotationOut(AnnotationIn, _CreatedAt, _Id): pass - - -class AnnotationUrl(BaseModel): - url: str diff --git a/src/app/db/models.py b/src/app/db/models.py index 826b7ac..28bee2d 100644 --- a/src/app/db/models.py +++ b/src/app/db/models.py @@ -5,7 +5,7 @@ import enum -from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String +from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, ARRAY from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -51,10 +51,10 @@ class Annotations(Base): id = Column(Integer, primary_key=True) media_id = Column(Integer, ForeignKey("media.id")) - bucket_key = Column(String(100), nullable=True) + observations = Column(ARRAY(String(50)), nullable=False) created_at = Column(DateTime, default=func.now()) media = relationship("Media", uselist=False, back_populates="annotations") def __repr__(self): - return f"" + return f"" diff --git a/src/tests/routes/test_annotations.py b/src/tests/routes/test_annotations.py index 71f00e1..2da4f8c 100644 --- a/src/tests/routes/test_annotations.py +++ b/src/tests/routes/test_annotations.py @@ -1,6 +1,4 @@ import json -import os -import tempfile from datetime import datetime import pytest @@ -8,8 +6,6 @@ from app import db from app.api import crud -from app.api.security import hash_content_file -from app.services import s3_bucket from tests.db_utils import TestSessionLocal, fill_table, get_entry from tests.utils import update_only_datetime @@ -24,7 +20,8 @@ ] ANNOTATIONS_TABLE = [ - {"id": 1, "media_id": 1, "bucket_key": "dummy_key", "created_at": "2020-10-13T08:18:45.447773"}, + {"id": 1, "media_id": 1, "observations": ["fire", "smoke", "dog", "coffee"], "created_at": "2020-10-13T08:18:45.447773"}, + {"id": 2, "media_id": 2, "observations": [], "created_at": "2022-10-13T08:18:45.447773"}, ] @@ -65,7 +62,7 @@ async def test_get_annotation(test_app_asyncio, init_test_db, access_idx, annota assert response.json()["detail"] == status_details if response.status_code // 100 == 2: - assert response.json() == {k: v for k, v in ANNOTATIONS_TABLE[annotation_id - 1].items() if k != "bucket_key"} + assert response.json() == ANNOTATIONS_TABLE[annotation_id - 1] @pytest.mark.parametrize( @@ -73,7 +70,7 @@ async def test_get_annotation(test_app_asyncio, init_test_db, access_idx, annota [ [None, 401, "Not authenticated", None], [0, 200, None, []], - [1, 200, None, [{k: v for k, v in elt.items() if k != "bucket_key"} for elt in ANNOTATIONS_TABLE]], + [1, 200, None, ANNOTATIONS_TABLE], ], ) @pytest.mark.asyncio @@ -164,8 +161,7 @@ async def test_update_annotation( updated_annotation = await get_entry(test_db, db.annotations, annotation_id) updated_annotation = dict(**updated_annotation) for k, v in updated_annotation.items(): - if k != "bucket_key": - assert v == payload.get(k, ANNOTATIONS_TABLE_FOR_DB[annotation_id - 1][k]) + assert v == payload.get(k, ANNOTATIONS_TABLE_FOR_DB[annotation_id - 1][k]) @pytest.mark.parametrize( @@ -194,71 +190,6 @@ async def test_delete_annotation( assert response.json()["detail"] == status_details if response.status_code // 100 == 2: - assert response.json() == {k: v for k, v in ANNOTATIONS_TABLE[annotation_id - 1].items() if k != "bucket_key"} + assert response.json() == ANNOTATIONS_TABLE[annotation_id - 1] remaining_annotation = await test_app_asyncio.get("/annotations/", headers=auth) assert all(entry["id"] != annotation_id for entry in remaining_annotation.json()) - - -@pytest.mark.asyncio -async def test_upload_annotation(test_app_asyncio, init_test_db, test_db, monkeypatch): - - admin_idx = 1 - # Create a custom access token - admin_auth = await pytest.get_token(ACCESS_TABLE[admin_idx]["id"], ACCESS_TABLE[admin_idx]["scope"].split()) - - # 1 - Create a annotation that will have an upload - payload = {"media_id": 2} - new_annotation_id = len(ANNOTATIONS_TABLE_FOR_DB) + 1 - response = await test_app_asyncio.post("/annotations/", data=json.dumps(payload), headers=admin_auth) - assert response.status_code == 201 - - # 2 - Upload something - async def mock_upload_file(bucket_key, file_binary): - return True - - monkeypatch.setattr(s3_bucket, "upload_file", mock_upload_file) - - # Download and save a temporary file - local_tmp_path = os.path.join(tempfile.gettempdir(), "my_temp_annotation.json") - data = {"label": "fire"} - with open(local_tmp_path, "w") as f: - json.dump(data, f) - - with open(local_tmp_path, "rb") as f: - md5_hash = hash_content_file(f.read(), use_md5=True) - - async def mock_get_file_metadata(bucket_key): - return {"ETag": md5_hash} - - monkeypatch.setattr(s3_bucket, "get_file_metadata", mock_get_file_metadata) - - async def mock_delete_file(filename): - return True - - monkeypatch.setattr(s3_bucket, "delete_file", mock_delete_file) - - # Switch content-type from JSON to multipart - del admin_auth["Content-Type"] - - with open(local_tmp_path, "rb") as content: - response = await test_app_asyncio.post( - f"/annotations/{new_annotation_id}/upload", files=dict(file=content), headers=admin_auth - ) - - assert response.status_code == 200, print(response.json()["detail"]) - response_json = response.json() - updated_annotation = await get_entry(test_db, db.annotations, response_json["id"]) - updated_annotation = dict(**updated_annotation) - response_json.pop("created_at") - assert {k: v for k, v in updated_annotation.items() if k not in ("created_at", "bucket_key")} == response_json - assert updated_annotation["bucket_key"] is not None - - # 2b - Upload failing - async def failing_upload(bucket_key, file_binary): - return False - - monkeypatch.setattr(s3_bucket, "upload_file", failing_upload) - response = await test_app_asyncio.post( - f"/annotations/{new_annotation_id}/upload", files=dict(file="bar"), headers=admin_auth - ) - assert response.status_code == 500 From ae0ed9b257258975b5d344d8623249877951edbc Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 15:24:50 +0200 Subject: [PATCH 02/22] fix: image path --- src/Dockerfile-dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dockerfile-dev b/src/Dockerfile-dev index fa1c961..51e97b1 100644 --- a/src/Dockerfile-dev +++ b/src/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM pyrostorage:python3.8-alpine3.10 +FROM pyronear/storage-api:python3.8-alpine3.10 # copy requirements file COPY requirements-dev.txt requirements-dev.txt From ceec8b5f1ac280348bbcd5d16c5e644003a84391 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 16:47:27 +0200 Subject: [PATCH 03/22] feat: add correct tests for annotations and observations --- src/app/api/schemas.py | 4 +-- src/app/db/models.py | 9 ++++++- src/tests/crud/test_authorizations.py | 2 +- src/tests/db_utils.py | 2 +- src/tests/routes/test_annotations.py | 39 ++++++++++++++++++--------- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/app/api/schemas.py b/src/app/api/schemas.py index a1e511c..7b11b78 100644 --- a/src/app/api/schemas.py +++ b/src/app/api/schemas.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, validator -from app.db.models import AccessType, MediaType +from app.db.models import AccessType, MediaType, ObservationType # Template classes @@ -88,7 +88,7 @@ class MediaUrl(BaseModel): # Annotation class AnnotationIn(BaseModel): media_id: int = Field(..., gt=0) - observations: List[str] = Field(..., min_items=0) + observations: List[ObservationType] class AnnotationOut(AnnotationIn, _CreatedAt, _Id): diff --git a/src/app/db/models.py b/src/app/db/models.py index 28bee2d..c571174 100644 --- a/src/app/db/models.py +++ b/src/app/db/models.py @@ -46,12 +46,19 @@ def __repr__(self): return f"" +class ObservationType(str, enum.Enum): + fire: str = "fire" + smoke: str = "smoke" + clouds: str = "clouds" + sky: str = "sky" + fog: str = "fog" + class Annotations(Base): __tablename__ = "annotations" id = Column(Integer, primary_key=True) media_id = Column(Integer, ForeignKey("media.id")) - observations = Column(ARRAY(String(50)), nullable=False) + observations = Column(ARRAY(Enum(ObservationType)), nullable=False) created_at = Column(DateTime, default=func.now()) media = relationship("Media", uselist=False, back_populates="annotations") diff --git a/src/tests/crud/test_authorizations.py b/src/tests/crud/test_authorizations.py index 9f0e2c4..a7b0bc9 100644 --- a/src/tests/crud/test_authorizations.py +++ b/src/tests/crud/test_authorizations.py @@ -19,7 +19,7 @@ ANNOTATIONS_TABLE = [ - {"id": 1, "media_id": 1, "created_at": "2020-10-13T08:18:45.447773"}, + {"id": 1, "media_id": 1, "observations": [], "created_at": "2020-10-13T08:18:45.447773"}, ] diff --git a/src/tests/db_utils.py b/src/tests/db_utils.py index 86d4e8e..ff32608 100644 --- a/src/tests/db_utils.py +++ b/src/tests/db_utils.py @@ -34,7 +34,7 @@ async def fill_table(test_db: Database, table: Table, entries: List[Dict[str, An are not incremented if the "id" field is included """ if remove_ids: - entries = [{k: v for k, v in x.items() if k != "id"} for x in entries] + entries = [{k: v for k, v in entry.items() if k != "id"} for entry in entries] query = table.insert().values(entries) await test_db.execute(query=query) diff --git a/src/tests/routes/test_annotations.py b/src/tests/routes/test_annotations.py index 2da4f8c..93da46d 100644 --- a/src/tests/routes/test_annotations.py +++ b/src/tests/routes/test_annotations.py @@ -9,6 +9,8 @@ from tests.db_utils import TestSessionLocal, fill_table, get_entry from tests.utils import update_only_datetime +from app.db.models import ObservationType + ACCESS_TABLE = [ {"id": 1, "login": "first_login", "hashed_password": "hashed_pwd", "scope": "user"}, {"id": 2, "login": "second_login", "hashed_password": "hashed_pwd", "scope": "admin"}, @@ -20,7 +22,7 @@ ] ANNOTATIONS_TABLE = [ - {"id": 1, "media_id": 1, "observations": ["fire", "smoke", "dog", "coffee"], "created_at": "2020-10-13T08:18:45.447773"}, + {"id": 1, "media_id": 1, "observations": [ObservationType.fire, ObservationType.smoke, ObservationType.clouds], "created_at": "2020-10-13T08:18:45.447773"}, {"id": 2, "media_id": 2, "observations": [], "created_at": "2022-10-13T08:18:45.447773"}, ] @@ -95,25 +97,31 @@ async def test_fetch_annotations( @pytest.mark.parametrize( "access_idx, payload, status_code, status_details", [ - [None, {"media_id": 1}, 401, "Not authenticated"], - [0, {"media_id": 1}, 201, None], - [1, {"media_id": 1}, 201, None], - [1, {"media_id": "alpha"}, 422, None], + [None, {"media_id": 1, "observations": []}, 401, "Not authenticated"], + [0, {"media_id": 1, "observations": []}, 201, None], + [1, {"media_id": 1, "observations": []}, 201, None], + [1, {"media_id": 1, "observations": ["clouds"]}, 201, None], + [1, {"media_id": 1, "observations": ["clouds", "fire", "smoke"]}, 201, None], + [1, {"media_id": 1, "observations": ["clouds", "fire", "puppy"]}, 422, None], + [1, {"media_id": 1, "observations": [1337]}, 422, None], + [1, {"media_id": 1, "observations": "smoke"}, 422, None], + [1, {"media_id": "alpha", "observations": []}, 422, None], [1, {}, 422, None], + [1, {"media_id": 1}, 422, None], + [1, {"observations": []}, 422, None], ], ) @pytest.mark.asyncio async def test_create_annotation( test_app_asyncio, init_test_db, test_db, access_idx, payload, status_code, status_details ): - # Create a custom access token auth = None if isinstance(access_idx, int): auth = await pytest.get_token(ACCESS_TABLE[access_idx]["id"], ACCESS_TABLE[access_idx]["scope"].split()) utc_dt = datetime.utcnow() - response = await test_app_asyncio.post("/annotations/", data=json.dumps(payload), headers=auth) + response = await test_app_asyncio.post("/annotations/", json=payload, headers=auth) assert response.status_code == status_code if isinstance(status_details, str): assert response.json()["detail"] == status_details @@ -133,13 +141,18 @@ async def test_create_annotation( @pytest.mark.parametrize( "access_idx, payload, annotation_id, status_code, status_details", [ - [None, {"media_id": 1}, 1, 401, "Not authenticated"], - [0, {"media_id": 1}, 1, 403, "Your access scope is not compatible with this operation."], - [1, {"media_id": 1}, 1, 200, None], + [None, {"media_id": 1, "observations": []}, 1, 401, "Not authenticated"], + [0, {"media_id": 1, "observations": []}, 1, 403, "Your access scope is not compatible with this operation."], + [1, {"media_id": 1, "observations": []}, 1, 200, None], + [1, {"media_id": 1, "observations": [1337]}, 1, 422, None], + [1, {"media_id": 1, "observations": ["smoke"]}, 1, 200, None], + [1, {"media_id": 1, "observations": ["smoke", "fire", "puppy"]}, 1, 422, None], [1, {}, 1, 422, None], - [1, {"media_id": "alpha"}, 1, 422, None], - [1, {"media_id": 1}, 999, 404, "Table annotations has no entry with id=999"], - [1, {"media_id": 1}, 0, 422, None], + [1, {"media_id": 1,}, 1, 422, None], + [1, {"observations": []}, 1, 422, None], + [1, {"media_id": "alpha", "observations": []}, 1, 422, None], + [1, {"media_id": 1, "observations": []}, 999, 404, "Table annotations has no entry with id=999"], + [1, {"media_id": 1, "observations": []}, 0, 422, None], ], ) @pytest.mark.asyncio From 3acab3b81977e0714e3ea95344ab7f1797a3ad6e Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 16:51:29 +0200 Subject: [PATCH 04/22] feat: sort types --- src/app/db/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/db/models.py b/src/app/db/models.py index c571174..5111af0 100644 --- a/src/app/db/models.py +++ b/src/app/db/models.py @@ -47,11 +47,11 @@ def __repr__(self): class ObservationType(str, enum.Enum): - fire: str = "fire" - smoke: str = "smoke" clouds: str = "clouds" - sky: str = "sky" + fire: str = "fire" fog: str = "fog" + sky: str = "sky" + smoke: str = "smoke" class Annotations(Base): __tablename__ = "annotations" From c1d8d3ca2896909da0a549015de4225ee0f2d7bd Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:26:25 +0200 Subject: [PATCH 05/22] feat: update only observations --- client/pyrostorage/client.py | 45 +++++++--------------------- client/tests/test_client.py | 2 +- src/app/api/routes/annotations.py | 6 ++-- src/app/api/schemas.py | 2 ++ src/tests/routes/test_annotations.py | 20 ++++++------- 5 files changed, 26 insertions(+), 49 deletions(-) diff --git a/client/pyrostorage/client.py b/client/pyrostorage/client.py index d95a917..f00def9 100644 --- a/client/pyrostorage/client.py +++ b/client/pyrostorage/client.py @@ -5,7 +5,7 @@ import io import logging -from typing import Dict +from typing import Dict, List from urllib.parse import urljoin import requests @@ -29,8 +29,7 @@ # ANNOTATIONS ################# "create-annotation": "/annotations", - "upload-annotation": "/annotations/{annotation_id}/upload", - "get-annotation-url": "/annotations/{annotation_id}/url", + "update-annotation": "/annotations/{annotation_id}", } @@ -129,7 +128,7 @@ def get_media_url(self, media_id: int) -> Response: return requests.get(self.routes["get-media-url"].format(media_id=media_id), headers=self.headers) - def create_annotation(self, media_id: int) -> Response: + def create_annotation(self, media_id: int, observations: List[str]) -> Response: """Create an annotation entry Example:: @@ -139,49 +138,27 @@ def create_annotation(self, media_id: int) -> Response: Args: media_id: the identifier of the media entry + observations: list of observations Returns: HTTP response containing the created annotation """ - return requests.post(self.routes["create-annotation"], headers=self.headers, json={"media_id": media_id}) + return requests.post(self.routes["create-annotation"], headers=self.headers, json={"media_id": media_id, observations: observations}) - def upload_annotation(self, annotation_id: int, annotation_data: bytes) -> Response: - """Upload the annotation content + def update_annotation(self, annotation_id: int, observations: List[str]=None) -> Response: + """Update an annotation entry Example:: >>> from pyrostorage import client >>> api_client = client.Client("http://pyro-storage.herokuapp.com", "MY_LOGIN", "MY_PWD") - >>> with open("path/to/my/file.ext", "rb") as f: data = f.read() - >>> response = api_client.upload_annotation(annotation_id=1, annotation_data=data) - - Args: - annotation_id: ID of the associated annotation entry - annotation_data: byte data - - Returns: - HTTP response containing the updated annotation - """ - - return requests.post( - self.routes["upload-annotation"].format(annotation_id=annotation_id), - headers=self.headers, - files={"file": io.BytesIO(annotation_data)}, - ) - - def get_annotation_url(self, annotation_id: int) -> Response: - """Get the image as a URL - - Example:: - >>> from pyrostorage import client - >>> api_client = client.Client("http://pyro-storage.herokuapp.com", "MY_LOGIN", "MY_PWD") - >>> response = api_client.get_annotation_url(1) + >>> response = api_client.update_annotation(media_id=1) Args: annotation_id: the identifier of the annotation entry + observations: list of observations Returns: - HTTP response containing the URL to the annotation content + HTTP response containing the updated annotation """ - - return requests.get(self.routes["get-annotation-url"].format(annotation_id=annotation_id), headers=self.headers) + return requests.post(f'self.routes["update-annotation"]/{annotation_id}', headers=self.headers, json={"observations": observations}) diff --git a/client/tests/test_client.py b/client/tests/test_client.py index 99e0bf4..26e2f5b 100644 --- a/client/tests/test_client.py +++ b/client/tests/test_client.py @@ -30,7 +30,7 @@ def test_client(): # Media media_id = _test_route_return(api_client.create_media(media_type="image"), dict, 201)["id"] # Annotation - _test_route_return(api_client.create_annotation(media_id=media_id), dict, 201)["id"] + _test_route_return(api_client.create_annotation(media_id=media_id, observations=["smoke", "fire"]), dict, 201)["id"] # Check token refresh prev_headers = deepcopy(api_client.headers) diff --git a/src/app/api/routes/annotations.py b/src/app/api/routes/annotations.py index 66dff63..e044d43 100644 --- a/src/app/api/routes/annotations.py +++ b/src/app/api/routes/annotations.py @@ -10,7 +10,7 @@ from app.api import crud from app.api.crud.authorizations import check_access_read, is_admin_access from app.api.deps import get_current_access -from app.api.schemas import AccessType, AnnotationIn, AnnotationOut +from app.api.schemas import AccessType, AnnotationIn, AnnotationOut, AnnotationUpdateIn from app.db import annotations router = APIRouter() @@ -64,9 +64,9 @@ async def fetch_annotations( return [] -@router.put("/{annotation_id}/", response_model=AnnotationOut, summary="Update information about a specific annotation") +@router.put("/{annotation_id}/", response_model=AnnotationOut, summary="Update observations on a specific annotation") async def update_annotation( - payload: AnnotationIn, + payload: AnnotationUpdateIn, annotation_id: int = Path(..., gt=0), _=Security(get_current_access, scopes=[AccessType.admin]), ): diff --git a/src/app/api/schemas.py b/src/app/api/schemas.py index 7b11b78..004a555 100644 --- a/src/app/api/schemas.py +++ b/src/app/api/schemas.py @@ -90,6 +90,8 @@ class AnnotationIn(BaseModel): media_id: int = Field(..., gt=0) observations: List[ObservationType] +class AnnotationUpdateIn(BaseModel): + observations: List[ObservationType] class AnnotationOut(AnnotationIn, _CreatedAt, _Id): pass diff --git a/src/tests/routes/test_annotations.py b/src/tests/routes/test_annotations.py index 93da46d..9af7e73 100644 --- a/src/tests/routes/test_annotations.py +++ b/src/tests/routes/test_annotations.py @@ -141,18 +141,16 @@ async def test_create_annotation( @pytest.mark.parametrize( "access_idx, payload, annotation_id, status_code, status_details", [ - [None, {"media_id": 1, "observations": []}, 1, 401, "Not authenticated"], - [0, {"media_id": 1, "observations": []}, 1, 403, "Your access scope is not compatible with this operation."], - [1, {"media_id": 1, "observations": []}, 1, 200, None], - [1, {"media_id": 1, "observations": [1337]}, 1, 422, None], - [1, {"media_id": 1, "observations": ["smoke"]}, 1, 200, None], - [1, {"media_id": 1, "observations": ["smoke", "fire", "puppy"]}, 1, 422, None], + [None, {"observations": []}, 1, 401, "Not authenticated"], + [0, {"observations": []}, 1, 403, "Your access scope is not compatible with this operation."], + [1, {"observations": []}, 1, 200, None], + [1, {"observations": [1337]}, 1, 422, None], + [1, {"observations": ["smoke"]}, 1, 200, None], + [1, {"observations": ["smoke", "fire", "puppy"]}, 1, 422, None], [1, {}, 1, 422, None], - [1, {"media_id": 1,}, 1, 422, None], - [1, {"observations": []}, 1, 422, None], - [1, {"media_id": "alpha", "observations": []}, 1, 422, None], - [1, {"media_id": 1, "observations": []}, 999, 404, "Table annotations has no entry with id=999"], - [1, {"media_id": 1, "observations": []}, 0, 422, None], + [1, {"observations": "smoke"}, 1, 422, None], + [1, {"observations": []}, 999, 404, "Table annotations has no entry with id=999"], + [1, {"observations": []}, 0, 422, None], ], ) @pytest.mark.asyncio From a7496a33b7097925f624ca741f65cab914771194 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:40:07 +0200 Subject: [PATCH 06/22] test: build docker wait for probing doc --- .github/workflows/builds.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 6c57759..9a30141 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -29,12 +29,12 @@ jobs: S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} S3_REGION: ${{ secrets.S3_REGION }} S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }} - run: docker-compose up -d --build - - name: Docker sanity check - run: sleep 20 && nc -vz localhost 8080 - - name: Ping server - run: curl http://localhost:8080/docs - + run: | + docker compose up -d --build --wait + sleep 20 + docker compose logs backend + nc -vz localhost 8080 + curl http://localhost:8080/docs client: runs-on: ${{ matrix.os }} strategy: From d26245c21056fc684d8bb2960bd60b59e1610601 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:45:11 +0200 Subject: [PATCH 07/22] feat: add health-check --- docker-compose.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3f9f2ea..606f7dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,11 @@ services: - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} depends_on: - db + healthcheck: + test: ["CMD-SHELL", "nc -vz localhost 8080"] + interval: 10s + timeout: 3s + retries: 3 db: image: postgres:15-alpine volumes: @@ -29,13 +34,11 @@ services: - POSTGRES_USER=dummy_pg_user - POSTGRES_PASSWORD=dummy_pg_pwd - POSTGRES_DB=dummy_pg_db - nginx: - build: nginx - ports: - - 80:80 - - 443:443 - depends_on: - - backend + healthcheck: + test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'"] + interval: 10s + timeout: 3s + retries: 3 volumes: postgres_data: From 20f218ec87c05c48a6f100ed2028374a17f95ecd Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:48:48 +0200 Subject: [PATCH 08/22] feat: remove hc on backend; --- docker-compose.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 606f7dd..b916513 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,11 +19,6 @@ services: - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} depends_on: - db - healthcheck: - test: ["CMD-SHELL", "nc -vz localhost 8080"] - interval: 10s - timeout: 3s - retries: 3 db: image: postgres:15-alpine volumes: From 620934b15e689e651d712a6c38ce28c43b281e2c Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:52:50 +0200 Subject: [PATCH 09/22] feat: hard-code env vars for db --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b916513..454b4de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: - POSTGRES_PASSWORD=dummy_pg_pwd - POSTGRES_DB=dummy_pg_db healthcheck: - test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'"] + test: ['CMD-SHELL', "sh -c 'pg_isready -U dummy_pg_user -d dummy_pg_db'"] interval: 10s timeout: 3s retries: 3 From 6b27953035db0bbaf84259590f5b6c7f7b33b844 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 17:54:54 +0200 Subject: [PATCH 10/22] feat: wait for condition healthy --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 454b4de..91d8768 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,8 @@ services: - S3_REGION=${S3_REGION} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} depends_on: - - db + db: + condition: service_healthy db: image: postgres:15-alpine volumes: From 5d89bf547e7db33c1abe351e0779b1c7f540edbc Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 18:02:06 +0200 Subject: [PATCH 11/22] feat: add health-checks in dev --- .github/workflows/builds.yml | 1 - docker-compose-dev.yml | 10 ++++++++++ docker-compose.yml | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 9a30141..a5bfe87 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -31,7 +31,6 @@ jobs: S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }} run: | docker compose up -d --build --wait - sleep 20 docker compose logs backend nc -vz localhost 8080 curl http://localhost:8080/docs diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index fabea73..61f3347 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -23,6 +23,11 @@ services: - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} depends_on: - db + healthcheck: + test: ['CMD-SHELL', 'nc -vz localhost 8080'] + interval: 10s + timeout: 3s + retries: 3 db: image: postgres:15-alpine volumes: @@ -33,6 +38,11 @@ services: - POSTGRES_USER=dummy_pg_user - POSTGRES_PASSWORD=dummy_pg_pwd - POSTGRES_DB=dummy_pg_db + healthcheck: + test: ['CMD-SHELL', "sh -c 'pg_isready -U dummy_pg_user -d dummy_pg_db'"] + interval: 10s + timeout: 3s + retries: 3 proxy: build: nginx ports: diff --git a/docker-compose.yml b/docker-compose.yml index 91d8768..ea6f831 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,11 @@ services: - S3_SECRET_KEY=${S3_SECRET_KEY} - S3_REGION=${S3_REGION} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL} + healthcheck: + test: ['CMD-SHELL', 'nc -vz localhost 8080'] + interval: 10s + timeout: 3s + retries: 3 depends_on: db: condition: service_healthy From 5267883b593b105fbaae5b00229e9e168baecc5c Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Apr 2024 18:13:30 +0200 Subject: [PATCH 12/22] feat: lint --- client/docs/source/conf.py | 1 + client/pyrostorage/client.py | 14 +++++++++++--- src/app/api/schemas.py | 2 ++ src/app/db/models.py | 3 ++- src/tests/routes/test_annotations.py | 10 +++++++--- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/client/docs/source/conf.py b/client/docs/source/conf.py index 3bc082f..e614472 100644 --- a/client/docs/source/conf.py +++ b/client/docs/source/conf.py @@ -104,6 +104,7 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] + # Add googleanalytics id # ref: https://github.com/orenhecht/googleanalytics/blob/master/sphinxcontrib/googleanalytics.py def add_ga_javascript(app, pagename, templatename, context, doctree): diff --git a/client/pyrostorage/client.py b/client/pyrostorage/client.py index f00def9..e8b3996 100644 --- a/client/pyrostorage/client.py +++ b/client/pyrostorage/client.py @@ -144,9 +144,13 @@ def create_annotation(self, media_id: int, observations: List[str]) -> Response: HTTP response containing the created annotation """ - return requests.post(self.routes["create-annotation"], headers=self.headers, json={"media_id": media_id, observations: observations}) + return requests.post( + self.routes["create-annotation"], + headers=self.headers, + json={"media_id": media_id, observations: observations}, + ) - def update_annotation(self, annotation_id: int, observations: List[str]=None) -> Response: + def update_annotation(self, annotation_id: int, observations: List[str] = None) -> Response: """Update an annotation entry Example:: @@ -161,4 +165,8 @@ def update_annotation(self, annotation_id: int, observations: List[str]=None) -> Returns: HTTP response containing the updated annotation """ - return requests.post(f'self.routes["update-annotation"]/{annotation_id}', headers=self.headers, json={"observations": observations}) + return requests.post( + f'self.routes["update-annotation"]/{annotation_id}', + headers=self.headers, + json={"observations": observations}, + ) diff --git a/src/app/api/schemas.py b/src/app/api/schemas.py index 004a555..513f0af 100644 --- a/src/app/api/schemas.py +++ b/src/app/api/schemas.py @@ -90,8 +90,10 @@ class AnnotationIn(BaseModel): media_id: int = Field(..., gt=0) observations: List[ObservationType] + class AnnotationUpdateIn(BaseModel): observations: List[ObservationType] + class AnnotationOut(AnnotationIn, _CreatedAt, _Id): pass diff --git a/src/app/db/models.py b/src/app/db/models.py index 5111af0..38a14bf 100644 --- a/src/app/db/models.py +++ b/src/app/db/models.py @@ -5,7 +5,7 @@ import enum -from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, ARRAY +from sqlalchemy import ARRAY, Column, DateTime, Enum, ForeignKey, Integer, String from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -53,6 +53,7 @@ class ObservationType(str, enum.Enum): sky: str = "sky" smoke: str = "smoke" + class Annotations(Base): __tablename__ = "annotations" diff --git a/src/tests/routes/test_annotations.py b/src/tests/routes/test_annotations.py index 9af7e73..365bfbb 100644 --- a/src/tests/routes/test_annotations.py +++ b/src/tests/routes/test_annotations.py @@ -6,11 +6,10 @@ from app import db from app.api import crud +from app.db.models import ObservationType from tests.db_utils import TestSessionLocal, fill_table, get_entry from tests.utils import update_only_datetime -from app.db.models import ObservationType - ACCESS_TABLE = [ {"id": 1, "login": "first_login", "hashed_password": "hashed_pwd", "scope": "user"}, {"id": 2, "login": "second_login", "hashed_password": "hashed_pwd", "scope": "admin"}, @@ -22,7 +21,12 @@ ] ANNOTATIONS_TABLE = [ - {"id": 1, "media_id": 1, "observations": [ObservationType.fire, ObservationType.smoke, ObservationType.clouds], "created_at": "2020-10-13T08:18:45.447773"}, + { + "id": 1, + "media_id": 1, + "observations": [ObservationType.fire, ObservationType.smoke, ObservationType.clouds], + "created_at": "2020-10-13T08:18:45.447773", + }, {"id": 2, "media_id": 2, "observations": [], "created_at": "2022-10-13T08:18:45.447773"}, ] From 62c4f9fb6d1d5579bd2144f64db45740a4462a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislas=20Bruhi=C3=A8re?= Date: Mon, 1 Jul 2024 14:17:58 +0200 Subject: [PATCH 13/22] chore: retrigger ci From 212cda3b2dbd2c4212788ce1ede5ead875038ba9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:26:55 -0700 Subject: [PATCH 14/22] chore(deps): Bump certifi from 2022.12.7 to 2023.7.22 (#29) Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22. - [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22) --- updated-dependencies: - dependency-name: certifi dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 98 ++++++----------------------------------------------- 1 file changed, 10 insertions(+), 88 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3e35258..f90adfc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "aiofiles" version = "0.6.0" description = "File support for asyncio." -category = "main" optional = false python-versions = "*" files = [ @@ -16,7 +15,6 @@ files = [ name = "aiosqlite" version = "0.19.0" description = "asyncio bridge to the standard sqlite3 module" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -32,7 +30,6 @@ docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] name = "anyio" version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.6.2" files = [ @@ -53,7 +50,6 @@ trio = ["trio (>=0.16,<0.22)"] name = "asyncpg" version = "0.27.0" description = "An asyncio PostgreSQL driver" -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -104,7 +100,6 @@ test = ["flake8 (>=5.0.4,<5.1.0)", "uvloop (>=0.15.3)"] name = "autoflake" version = "1.7.8" description = "Removes unused imports and unused variables" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -120,7 +115,6 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} name = "bandit" version = "1.7.5" description = "Security oriented static analyser for python code." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -145,7 +139,6 @@ yaml = ["PyYAML"] name = "bcrypt" version = "3.2.2" description = "Modern password hashing for your software and your servers" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -173,7 +166,6 @@ typecheck = ["mypy"] name = "black" version = "22.3.0" description = "The uncompromising code formatter." -category = "main" optional = true python-versions = ">=3.6.2" files = [ @@ -220,7 +212,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "boto3" version = "1.26.123" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -240,7 +231,6 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "botocore" version = "1.29.123" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -258,21 +248,19 @@ crt = ["awscrt (==0.16.9)"] [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = "*" files = [ @@ -349,7 +337,6 @@ pycparser = "*" name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." -category = "main" optional = true python-versions = ">=3.6.1" files = [ @@ -361,7 +348,6 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -446,7 +432,6 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -461,7 +446,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -473,7 +457,6 @@ files = [ name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -536,7 +519,6 @@ toml = ["tomli"] name = "databases" version = "0.4.0" description = "Async database support for Python." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -559,7 +541,6 @@ sqlite = ["aiosqlite"] name = "distlib" version = "0.3.6" description = "Distribution utilities" -category = "main" optional = true python-versions = "*" files = [ @@ -571,7 +552,6 @@ files = [ name = "ecdsa" version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -590,7 +570,6 @@ gmpy2 = ["gmpy2"] name = "exceptiongroup" version = "1.1.1" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -605,7 +584,6 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.95.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -627,7 +605,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6 name = "filelock" version = "3.12.0" description = "A platform independent file lock." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -643,7 +620,6 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "gitdb" version = "4.0.10" description = "Git Object Database" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -658,7 +634,6 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.31" description = "GitPython is a Python library used to interact with Git repositories" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -673,7 +648,6 @@ gitdb = ">=4.0.1,<5" name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -747,7 +721,6 @@ test = ["objgraph", "psutil"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -759,7 +732,6 @@ files = [ name = "httpcore" version = "0.17.0" description = "A minimal low-level HTTP client." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -771,17 +743,16 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" version = "0.24.0" description = "The next generation HTTP client." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -797,15 +768,14 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "identify" version = "2.5.23" description = "File identification library for Python" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -820,7 +790,6 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -832,7 +801,6 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -844,7 +812,6 @@ files = [ name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "main" optional = true python-versions = ">=3.8.0" files = [ @@ -862,7 +829,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -874,7 +840,6 @@ files = [ name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -899,7 +864,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -911,7 +875,6 @@ files = [ name = "mypy" version = "1.2.0" description = "Optional static typing for Python" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -958,7 +921,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "main" optional = true python-versions = ">=3.5" files = [ @@ -970,7 +932,6 @@ files = [ name = "nodeenv" version = "1.7.0" description = "Node.js virtual environment builder" -category = "main" optional = true python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -985,7 +946,6 @@ setuptools = "*" name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -997,7 +957,6 @@ files = [ name = "passlib" version = "1.7.4" description = "comprehensive password hashing framework supporting over 30 schemes" -category = "main" optional = false python-versions = "*" files = [ @@ -1018,7 +977,6 @@ totp = ["cryptography"] name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1030,7 +988,6 @@ files = [ name = "pbr" version = "5.11.1" description = "Python Build Reasonableness" -category = "main" optional = true python-versions = ">=2.6" files = [ @@ -1042,7 +999,6 @@ files = [ name = "platformdirs" version = "3.5.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1058,7 +1014,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1074,7 +1029,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1093,7 +1047,6 @@ virtualenv = ">=20.10.0" name = "psycopg2" version = "2.9.6" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1116,7 +1069,6 @@ files = [ name = "pyasn1" version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1128,7 +1080,6 @@ files = [ name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1140,7 +1091,6 @@ files = [ name = "pydantic" version = "1.10.7" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1193,7 +1143,6 @@ email = ["email-validator (>=1.0.3)"] name = "pydocstyle" version = "6.3.0" description = "Python docstring style checker" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1212,7 +1161,6 @@ toml = ["tomli (>=1.2.3)"] name = "pyflakes" version = "2.5.0" description = "passive checker of Python programs" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1224,7 +1172,6 @@ files = [ name = "pygments" version = "2.15.1" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1239,7 +1186,6 @@ plugins = ["importlib-metadata"] name = "pytest" version = "7.3.1" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1262,7 +1208,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.21.0" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1281,7 +1226,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1296,7 +1240,6 @@ six = ">=1.5" name = "python-jose" version = "3.3.0" description = "JOSE implementation in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -1318,7 +1261,6 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] name = "python-multipart" version = "0.0.5" description = "A streaming multipart parser for Python" -category = "main" optional = false python-versions = "*" files = [ @@ -1332,7 +1274,6 @@ six = ">=1.4.0" name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1382,7 +1323,6 @@ files = [ name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1404,7 +1344,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rich" version = "13.3.5" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" optional = true python-versions = ">=3.7.0" files = [ @@ -1424,7 +1363,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -1439,7 +1377,6 @@ pyasn1 = ">=0.1.3" name = "ruff" version = "0.0.263" description = "An extremely fast Python linter, written in Rust." -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1466,7 +1403,6 @@ files = [ name = "s3transfer" version = "0.6.0" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = ">= 3.7" files = [ @@ -1484,7 +1420,6 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] name = "sentry-sdk" version = "1.21.1" description = "Python client for Sentry (https://sentry.io)" -category = "main" optional = false python-versions = "*" files = [ @@ -1526,7 +1461,6 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "67.7.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1543,7 +1477,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1555,7 +1488,6 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1567,7 +1499,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1579,7 +1510,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "main" optional = true python-versions = "*" files = [ @@ -1591,7 +1521,6 @@ files = [ name = "sqlalchemy" version = "1.4.47" description = "Database Abstraction Library" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1639,7 +1568,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] @@ -1666,7 +1595,6 @@ sqlcipher = ["sqlcipher3-binary"] name = "starlette" version = "0.26.1" description = "The little ASGI library that shines." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1685,7 +1613,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "stevedore" version = "5.0.0" description = "Manage dynamic plugins for Python applications" -category = "main" optional = true python-versions = ">=3.8" files = [ @@ -1700,7 +1627,6 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1712,7 +1638,6 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1724,7 +1649,6 @@ files = [ name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1741,7 +1665,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "uvicorn" version = "0.22.0" description = "The lightning-fast ASGI server." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1760,7 +1683,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "virtualenv" version = "20.23.0" description = "Virtual Python Environment builder" -category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1778,7 +1700,7 @@ docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx- test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"] [extras] -quality = ["ruff", "isort", "mypy", "pydocstyle", "black", "autoflake", "bandit", "pre-commit"] +quality = ["autoflake", "bandit", "black", "isort", "mypy", "pre-commit", "pydocstyle", "ruff"] [metadata] lock-version = "2.0" From ee5f16e81276ceb13d4d1849f60deafb1b9d10ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Sep 2023 18:32:40 +0200 Subject: [PATCH 15/22] chore(deps): Bump gitpython from 3.1.31 to 3.1.34 (#31) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.31 to 3.1.34. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.31...3.1.34) --- updated-dependencies: - dependency-name: gitpython dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index f90adfc..5d55526 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiofiles" @@ -620,7 +620,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "gitdb" version = "4.0.10" description = "Git Object Database" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, @@ -632,13 +632,13 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.31" +version = "3.1.34" description = "GitPython is a Python library used to interact with Git repositories" -optional = true +optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, - {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, + {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"}, + {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"}, ] [package.dependencies] @@ -656,6 +656,7 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -664,6 +665,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -693,6 +695,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -701,6 +704,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -1488,7 +1492,7 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, @@ -1568,7 +1572,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] From b7463275505b15e29e3ca0335a0060597bcf3a28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Sep 2023 18:37:11 +0200 Subject: [PATCH 16/22] chore(deps): Bump gitpython from 3.1.34 to 3.1.35 (#32) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.34 to 3.1.35. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.34...3.1.35) --- updated-dependencies: - dependency-name: gitpython dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5d55526..08f27c0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -632,13 +632,13 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.34" +version = "3.1.35" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.34-py3-none-any.whl", hash = "sha256:5d3802b98a3bae1c2b8ae0e1ff2e4aa16bcdf02c145da34d092324f599f01395"}, - {file = "GitPython-3.1.34.tar.gz", hash = "sha256:85f7d365d1f6bf677ae51039c1ef67ca59091c7ebd5a3509aa399d4eda02d6dd"}, + {file = "GitPython-3.1.35-py3-none-any.whl", hash = "sha256:c19b4292d7a1d3c0f653858db273ff8a6614100d1eb1528b014ec97286193c09"}, + {file = "GitPython-3.1.35.tar.gz", hash = "sha256:9cbefbd1789a5fe9bcf621bb34d3f441f3a90c8461d377f84eda73e721d9b06b"}, ] [package.dependencies] From 64941bd7d477bce7727965ec88cc1c12604e1ae7 Mon Sep 17 00:00:00 2001 From: F-G Fernandez <26927750+frgfm@users.noreply.github.com> Date: Sat, 16 Sep 2023 18:38:46 +0200 Subject: [PATCH 17/22] feat: Enables CORS using FastAPI middleware (#26) * feat: Adds CORS middleware * chore: Updates docker * docs: Updates README --- README.md | 1 + docker-compose.yml | 1 + src/app/config.py | 4 +++- src/app/main.py | 11 +++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c459df9..95283bf 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ This file will have to hold the following information: Optionally, the following information can be added: - `SENTRY_DSN`: the URL of the [Sentry](https://sentry.io/) project, which monitors back-end errors and report them back. - `SERVER_NAME`: the server tag to apply to events. +- `CORS_ORIGIN`: comma-separated list of allowed origins So your `.env` file should look like something similar to: ``` diff --git a/docker-compose.yml b/docker-compose.yml index ea6f831..8fc17d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - DATABASE_URL=postgresql://dummy_pg_user:dummy_pg_pwd@db/dummy_pg_db - SUPERUSER_LOGIN=dummy_login - SUPERUSER_PWD=dummy_pwd + - CORS_ORIGIN=${CORS_ORIGIN} - BUCKET_NAME=${BUCKET_NAME} - S3_ACCESS_KEY=${S3_ACCESS_KEY} - S3_SECRET_KEY=${S3_SECRET_KEY} diff --git a/src/app/config.py b/src/app/config.py index 4066966..6ef091e 100644 --- a/src/app/config.py +++ b/src/app/config.py @@ -5,7 +5,7 @@ import os import secrets -from typing import Optional +from typing import List, Optional PROJECT_NAME: str = "Pyronear - Storage API" PROJECT_DESCRIPTION: str = "API for wildfire data curation" @@ -30,6 +30,8 @@ ACCESS_TOKEN_UNLIMITED_MINUTES = 60 * 24 * 365 * 10 JWT_ENCODING_ALGORITHM = "HS256" +CORS_ORIGIN: List[str] = os.getenv("CORS_ORIGIN", "*").split(",") + SUPERUSER_LOGIN: str = os.getenv("SUPERUSER_LOGIN", "") SUPERUSER_PWD: str = os.getenv("SUPERUSER_PWD", "") diff --git a/src/app/main.py b/src/app/main.py index 4165b18..0bcca41 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -8,6 +8,7 @@ import sentry_sdk from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.utils import get_openapi from sentry_sdk.integrations.asgi import SentryAsgiMiddleware @@ -63,6 +64,16 @@ async def add_process_time_header(request: Request, call_next): return response +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=cfg.CORS_ORIGIN, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + if isinstance(cfg.SENTRY_DSN, str): app.add_middleware(SentryAsgiMiddleware) From a6df74b6aa807a8e16501f8ca97db45013aa2604 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Oct 2023 19:04:30 +0200 Subject: [PATCH 18/22] chore(deps): Bump urllib3 from 1.26.15 to 1.26.17 (#33) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.15 to 1.26.17. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.15...1.26.17) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 08f27c0..42ee974 100644 --- a/poetry.lock +++ b/poetry.lock @@ -620,7 +620,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "gitdb" version = "4.0.10" description = "Git Object Database" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, @@ -634,7 +634,7 @@ smmap = ">=3.0.1,<6" name = "gitpython" version = "3.1.35" description = "GitPython is a Python library used to interact with Git repositories" -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "GitPython-3.1.35-py3-none-any.whl", hash = "sha256:c19b4292d7a1d3c0f653858db273ff8a6614100d1eb1528b014ec97286193c09"}, @@ -1492,7 +1492,7 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, @@ -1651,17 +1651,17 @@ files = [ [[package]] name = "urllib3" -version = "1.26.15" +version = "1.26.17" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, - {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, + {file = "urllib3-1.26.17-py2.py3-none-any.whl", hash = "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b"}, + {file = "urllib3-1.26.17.tar.gz", hash = "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] From 90d03269059f857cd485e424955cfac60c27b8d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 19:47:56 +0200 Subject: [PATCH 19/22] chore(deps): Bump gitpython from 3.1.35 to 3.1.37 (#34) Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.35 to 3.1.37. - [Release notes](https://github.com/gitpython-developers/GitPython/releases) - [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES) - [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.35...3.1.37) --- updated-dependencies: - dependency-name: gitpython dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 42ee974..3742250 100644 --- a/poetry.lock +++ b/poetry.lock @@ -620,7 +620,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "p name = "gitdb" version = "4.0.10" description = "Git Object Database" -optional = true +optional = false python-versions = ">=3.7" files = [ {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, @@ -632,18 +632,21 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.35" +version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" -optional = true +optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.35-py3-none-any.whl", hash = "sha256:c19b4292d7a1d3c0f653858db273ff8a6614100d1eb1528b014ec97286193c09"}, - {file = "GitPython-3.1.35.tar.gz", hash = "sha256:9cbefbd1789a5fe9bcf621bb34d3f441f3a90c8461d377f84eda73e721d9b06b"}, + {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, + {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] + [[package]] name = "greenlet" version = "2.0.2" @@ -1492,7 +1495,7 @@ files = [ name = "smmap" version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" -optional = true +optional = false python-versions = ">=3.6" files = [ {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, From ebc41042e38744510babcfc49662f04f7f497f19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 19:48:10 +0200 Subject: [PATCH 20/22] chore(deps): Bump urllib3 from 1.26.17 to 1.26.18 (#35) Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18) --- updated-dependencies: - dependency-name: urllib3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3742250..61bc3f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1654,13 +1654,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.17" +version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.17-py2.py3-none-any.whl", hash = "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b"}, - {file = "urllib3-1.26.17.tar.gz", hash = "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21"}, + {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, + {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, ] [package.extras] From 0acfed9c92ed6e83f04e948ec9a13eb6b1c005be Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Mon, 1 Jul 2024 23:50:18 +0200 Subject: [PATCH 21/22] fix: use string as json key --- client/pyrostorage/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyrostorage/client.py b/client/pyrostorage/client.py index e8b3996..40ef691 100644 --- a/client/pyrostorage/client.py +++ b/client/pyrostorage/client.py @@ -147,7 +147,7 @@ def create_annotation(self, media_id: int, observations: List[str]) -> Response: return requests.post( self.routes["create-annotation"], headers=self.headers, - json={"media_id": media_id, observations: observations}, + json={"media_id": media_id, "observations": observations}, ) def update_annotation(self, annotation_id: int, observations: List[str] = None) -> Response: From 43e4a50ae2c85274665b3690c2754e2a9d3e2b59 Mon Sep 17 00:00:00 2001 From: Stanislas Bruhiere Date: Tue, 2 Jul 2024 00:05:08 +0200 Subject: [PATCH 22/22] fix: no explicit none --- client/pyrostorage/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyrostorage/client.py b/client/pyrostorage/client.py index 40ef691..e1ed0cf 100644 --- a/client/pyrostorage/client.py +++ b/client/pyrostorage/client.py @@ -150,7 +150,7 @@ def create_annotation(self, media_id: int, observations: List[str]) -> Response: json={"media_id": media_id, "observations": observations}, ) - def update_annotation(self, annotation_id: int, observations: List[str] = None) -> Response: + def update_annotation(self, annotation_id: int, observations: List[str]) -> Response: """Update an annotation entry Example::