diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..5e39fc2b6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "flask-admin (Python 3.12)", + "image": "mcr.microsoft.com/devcontainers/python:3.12-bullseye", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.devcontainer/tests/Dockerfile b/.devcontainer/tests/Dockerfile new file mode 100644 index 000000000..e23ad4279 --- /dev/null +++ b/.devcontainer/tests/Dockerfile @@ -0,0 +1,6 @@ +ARG IMAGE=bullseye +FROM mcr.microsoft.com/devcontainers/${IMAGE} + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends postgresql-client \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* diff --git a/.devcontainer/tests/devcontainer.json b/.devcontainer/tests/devcontainer.json new file mode 100644 index 000000000..871b8a67b --- /dev/null +++ b/.devcontainer/tests/devcontainer.json @@ -0,0 +1,21 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "flask-admin tests (Postgres + Azurite + Mongo)", + "dockerComposeFile": "docker-compose.yaml", + "service": "app", + "workspaceFolder": "/workspace", + "forwardPorts": [10000, 10001, 5432, 27017], + "portsAttributes": { + "10000": {"label": "Azurite Blob Storage Emulator", "onAutoForward": "silent"}, + "10001": {"label": "Azurite Blob Storage Emulator HTTPS", "onAutoForward": "silent"}, + "5432": {"label": "PostgreSQL port", "onAutoForward": "silent"}, + "27017": {"label": "MongoDB port", "onAutoForward": "silent"}, + }, + "features": { + // For authenticating to a production Azure account + "ghcr.io/devcontainers/features/azure-cli:1": {} + }, + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "postAttachCommand": "pip install -e \".[all]\" && pip install --use-pep517 -r requirements/dev.txt && psql -U postgres -h localhost -c 'CREATE EXTENSION IF NOT EXISTS hstore;' flask_admin_test" +} diff --git a/.devcontainer/tests/docker-compose.yaml b/.devcontainer/tests/docker-compose.yaml new file mode 100644 index 000000000..5d7cab4ac --- /dev/null +++ b/.devcontainer/tests/docker-compose.yaml @@ -0,0 +1,44 @@ +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile + args: + IMAGE: python:3.12 + + volumes: + - ../..:/workspace:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + environment: + AZURE_STORAGE_CONNECTION_STRING: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1; + + postgres: + image: postgis/postgis:16-3.4 + restart: unless-stopped + environment: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: flask_admin_test + volumes: + - postgres-data:/var/lib/postgresql/data + network_mode: service:app + + azurite: + container_name: azurite + image: mcr.microsoft.com/azure-storage/azurite:latest + restart: unless-stopped + volumes: + - azurite-data:/data + network_mode: service:app + + mongo: + image: mongo:5.0.14-focal + restart: unless-stopped + network_mode: service:app + +volumes: + postgres-data: + azurite-data: diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b82ae8220..acd9516fd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -55,7 +55,7 @@ jobs: ports: - 27017:27017 azurite: - image: arafato/azurite:2.6.5 + image: mcr.microsoft.com/azure-storage/azurite:latest env: executable: blob ports: diff --git a/README.md b/README.md index 7587c81d1..d7e0c48f8 100644 --- a/README.md +++ b/README.md @@ -131,11 +131,13 @@ You should see output similar to: OK -**NOTE!** For all the tests to pass successfully, you\'ll need Postgres (with -the postgis and hstore extension) & MongoDB to be running locally. You'll -also need *libgeos* available. +**NOTE!** For all the tests to pass successfully, you\'ll need several services running locally: +Postgres (with the postgis and hstore extension), MongoDB, and Azurite. +You'll also need *libgeos* available. +See tests.yaml for Docker configuration and follow service-specific setup below. + +## Setting up local Postgres for tests -For Postgres: ```bash psql postgres > CREATE DATABASE flask_admin_test; @@ -144,6 +146,7 @@ psql postgres > CREATE EXTENSION postgis; > CREATE EXTENSION hstore; ``` + If you\'re using Homebrew on MacOS, you might need this: ```bash @@ -156,6 +159,16 @@ createuser -s postgresql brew services restart postgresql ``` +## Setting up Azure Blob Storage emulator for tests + +1. Run the [Azurite emulator](https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage) + +2. Set the connection string for the emulator: + +```bash +export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" +``` + You can also run the tests on multiple environments using *tox*. ## 3rd Party Stuff diff --git a/doc/changelog.rst b/doc/changelog.rst index b247441eb..2dcc098a1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -4,6 +4,10 @@ Changelog 2.0.0a3 ------- +Breaking changes: + +* Azure Blob Storage SDK has been upgraded from the legacy version (v2) to the latest version (v12). AzureFileAdmin now accept `blob_service_client` rather than `connection_string` to give more flexibility with connection types. + Fixes: * Jinja templates can now be loaded in StrictUndefined mode. diff --git a/examples/azure-blob-storage/README.md b/examples/azure-blob-storage/README.md new file mode 100644 index 000000000..f5f7d48ca --- /dev/null +++ b/examples/azure-blob-storage/README.md @@ -0,0 +1,33 @@ +# Azure Blob Storage Example + +Flask-Admin example for an Azure Blob Storage account. + +If you opened this repository in GitHub Codespaces or a Dev Container with the ["flask-admin tests" configuration](/.devcontainer/tests/devcontainer.json), you can jump straight to step 4. + +To run this example: + +1. Clone the repository and navigate to this example:: + + git clone https://github.com/pallets-eco/flask-admin.git + cd flask-admin/examples/azure-blob-storage + +2. Create and activate a virtual environment:: + + python -m venv venv + source venv/bin/activate + +3. Configure a connection to an Azure Blob storage account or local emulator. + + To connect to the Azurite Blob Storage Emulator, install Azurite and set the following environment variable: + + export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;" + + To connect to an Azure Blob Storage account, set the `AZURE_STORAGE_ACCOUNT_URL`. If you set that, the example assumes you are using keyless authentication, so you will need to be logged in via the Azure CLI. + +4. Install requirements:: + + pip install -r requirements.txt + +5. Run the application:: + + python app.py diff --git a/examples/azure-blob-storage/__init__.py b/examples/azure-blob-storage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/azure-blob-storage/app.py b/examples/azure-blob-storage/app.py new file mode 100644 index 000000000..b149730bf --- /dev/null +++ b/examples/azure-blob-storage/app.py @@ -0,0 +1,39 @@ +import logging +import os + +from azure.identity import DefaultAzureCredential +from azure.storage.blob import BlobServiceClient +from flask import Flask +from flask_admin import Admin +from flask_admin.contrib.fileadmin.azure import AzureFileAdmin +from flask_babel import Babel + +logging.basicConfig(level=logging.INFO) +app = Flask(__name__) +app.config["SECRET_KEY"] = "secret" + + +@app.route("/") +def index(): + return 'Click me to get to Admin!' + + +admin = Admin(app) +babel = Babel(app) + +if account_url := os.getenv("AZURE_STORAGE_ACCOUNT_URL"): + # https://learn.microsoft.com/azure/storage/blobs/storage-blob-python-get-started?tabs=azure-ad#authorize-access-and-connect-to-blob-storage + logging.info("Connecting to Azure Blob storage with keyless auth") + client = BlobServiceClient(account_url, credential=DefaultAzureCredential()) +elif conn_str := os.getenv("AZURE_STORAGE_CONNECTION_STRING"): + logging.info("Connecting to Azure Blob storage with connection string.") + client = BlobServiceClient.from_connection_string(conn_str) + +file_admin = AzureFileAdmin( + blob_service_client=client, + container_name="fileadmin-tests", +) +admin.add_view(file_admin) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/examples/azure-blob-storage/requirements.txt b/examples/azure-blob-storage/requirements.txt new file mode 100644 index 000000000..a795d4adb --- /dev/null +++ b/examples/azure-blob-storage/requirements.txt @@ -0,0 +1,2 @@ +../..[azure-blob-storage] +azure-identity diff --git a/flask_admin/contrib/fileadmin/azure.py b/flask_admin/contrib/fileadmin/azure.py index b72389f67..e0d32b2b6 100644 --- a/flask_admin/contrib/fileadmin/azure.py +++ b/flask_admin/contrib/fileadmin/azure.py @@ -1,15 +1,21 @@ +import io import os.path as op +import time from datetime import datetime from datetime import timedelta -from time import sleep try: - from azure.storage.blob import BlobPermissions - from azure.storage.blob import BlockBlobService -except ImportError: - BlobPermissions = BlockBlobService = None - -from flask import redirect + from azure.core.exceptions import ResourceExistsError + from azure.storage.blob import BlobProperties + from azure.storage.blob import BlobServiceClient +except ImportError as e: + raise Exception( + "Could not import `azure.storage.blob`. " + "Enable `azure-blob-storage` integration " + "by installing `flask-admin[azure-blob-storage]`" + ) from e + +import flask from . import BaseFileAdmin @@ -37,38 +43,30 @@ class MyAzureAdmin(BaseFileAdmin): _send_file_validity = timedelta(hours=1) separator = "/" - def __init__(self, container_name, connection_string): + def __init__(self, blob_service_client: BlobServiceClient, container_name: str): """ Constructor + :param blob_service_client: + BlobServiceClient for the Azure Blob Storage account + :param container_name: Name of the container that the files are on. - - :param connection_string: - Azure Blob Storage Connection String """ - - if not BlockBlobService: - raise ValueError( - "Could not import `azure.storage.blob`. " - "Enable `azure-blob-storage` integration " - "by installing `flask-admin[azure-blob-storage]`" - ) - + self._client = blob_service_client self._container_name = container_name - self._connection_string = connection_string - self.__client = None + try: + self._client.create_container(self._container_name) + except ResourceExistsError: + pass @property - def _client(self): - if not self.__client: - self.__client = BlockBlobService(connection_string=self._connection_string) - self.__client.create_container(self._container_name, fail_on_exist=False) - return self.__client + def _container_client(self): + return self._client.get_container_client(self._container_name) @classmethod - def _get_blob_last_modified(cls, blob): - last_modified = blob.properties.last_modified + def _get_blob_last_modified(cls, blob: BlobProperties): + last_modified = blob.last_modified tzinfo = last_modified.tzinfo epoch = last_modified - datetime(1970, 1, 1, tzinfo=tzinfo) return epoch.total_seconds() @@ -90,10 +88,13 @@ def get_files(self, path, directory): path_parts = path.split(self.separator) if path else [] num_path_parts = len(path_parts) - folders = set() + files = [] + directories = [] + + container_client = self._client.get_container_client(self._container_name) - for blob in self._client.list_blobs(self._container_name, path): + for blob in container_client.list_blobs(path): blob_path_parts = blob.name.split(self.separator) name = blob_path_parts.pop() @@ -103,40 +104,29 @@ def get_files(self, path, directory): if blob_is_file_at_current_level and not blob_is_directory_file: rel_path = blob.name is_dir = False - size = blob.properties.content_length + size = blob.size last_modified = self._get_blob_last_modified(blob) files.append((name, rel_path, is_dir, size, last_modified)) else: next_level_folder = blob_path_parts[: num_path_parts + 1] - folder_name = self.separator.join(next_level_folder) - folders.add(folder_name) - - folders.discard(directory) - for folder in folders: - name = folder.split(self.separator)[-1] - rel_path = folder - is_dir = True - size = 0 - last_modified = 0 - files.append((name, rel_path, is_dir, size, last_modified)) + rel_path = self.separator.join(next_level_folder) + name = rel_path.split(self.separator)[-1] + if directory and rel_path == directory: + continue + is_dir = True + size = 0 + last_modified = self._get_blob_last_modified(blob) + directories.append((name, rel_path, is_dir, size, last_modified)) - return files + return directories + files def is_dir(self, path): path = self._ensure_blob_path(path) - num_blobs = 0 - for blob in self._client.list_blobs(self._container_name, path): - blob_path_parts = blob.name.split(self.separator) - is_explicit_directory = blob_path_parts[-1] == self._fakedir - if is_explicit_directory: + blobs = self._container_client.list_blobs(name_starts_with=path) + for blob in blobs: + if blob.name != path: return True - - num_blobs += 1 - path_cannot_be_leaf = num_blobs >= 2 - if path_cannot_be_leaf: - return True - return False def path_exists(self, path): @@ -145,12 +135,13 @@ def path_exists(self, path): if path == self.get_base_path(): return True - try: - next(iter(self._client.list_blobs(self._container_name, path))) - except StopIteration: + if path is None: return False - else: + + # Return true if it exists as either a directory or a file + for _ in self._container_client.list_blobs(name_starts_with=path): return True + return False def get_base_path(self): return "" @@ -160,80 +151,94 @@ def get_breadcrumbs(self, path): accumulator = [] breadcrumbs = [] - for folder in path.split(self.separator): - accumulator.append(folder) - breadcrumbs.append((folder, self.separator.join(accumulator))) + if path is not None: + for folder in path.split(self.separator): + accumulator.append(folder) + breadcrumbs.append((folder, self.separator.join(accumulator))) return breadcrumbs def send_file(self, file_path): - file_path = self._ensure_blob_path(file_path) - - if not self._client.exists(self._container_name, file_path): - raise ValueError() - - now = datetime.utcnow() - url = self._client.make_blob_url(self._container_name, file_path) - sas = self._client.generate_blob_shared_access_signature( - self._container_name, - file_path, - BlobPermissions.READ, - expiry=now + self._send_file_validity, - start=now - self._send_file_lookback, + path = self._ensure_blob_path(file_path) + if path is None: + raise ValueError("No path provided") + blob = self._container_client.get_blob_client(path).download_blob() + if not blob.properties or not blob.properties.has_key("content_settings"): + raise ValueError("Blob has no properties") + mime_type = blob.properties["content_settings"]["content_type"] + blob_file = io.BytesIO() + blob.readinto(blob_file) + blob_file.seek(0) + return flask.send_file( + blob_file, mimetype=mime_type, as_attachment=True, download_name=path ) - return redirect(f"{url}?{sas}") def read_file(self, path): path = self._ensure_blob_path(path) - - blob = self._client.get_blob_to_bytes(self._container_name, path) - return blob.content + if path is None: + raise ValueError("No path provided") + blob = self._container_client.get_blob_client(path).download_blob() + return blob.readall() def write_file(self, path, content): path = self._ensure_blob_path(path) - - self._client.create_blob_from_text(self._container_name, path, content) + if path is None: + raise ValueError("No path provided") + self._container_client.upload_blob(path, content, overwrite=True) def save_file(self, path, file_data): path = self._ensure_blob_path(path) - - self._client.create_blob_from_stream( - self._container_name, path, file_data.stream - ) + if path is None: + raise ValueError("No path provided") + self._container_client.upload_blob(path, file_data.stream) def delete_tree(self, directory): directory = self._ensure_blob_path(directory) - for blob in self._client.list_blobs(self._container_name, directory): - self._client.delete_blob(self._container_name, blob.name) + for blob in self._container_client.list_blobs(directory): + self._container_client.delete_blob(blob.name) def delete_file(self, file_path): file_path = self._ensure_blob_path(file_path) - - self._client.delete_blob(self._container_name, file_path) + if file_path is None: + raise ValueError("No path provided") + self._container_client.delete_blob(file_path) def make_dir(self, path, directory): path = self._ensure_blob_path(path) directory = self._ensure_blob_path(directory) - + if path is None or directory is None: + raise ValueError("No path provided") blob = self.separator.join([path, directory, self._fakedir]) blob = blob.lstrip(self.separator) - self._client.create_blob_from_text(self._container_name, blob, "") + self._container_client.upload_blob(blob, b"") def _copy_blob(self, src, dst): - src_url = self._client.make_blob_url(self._container_name, src) - copy = self._client.copy_blob(self._container_name, dst, src_url) - while copy.status != "success": - sleep(self._copy_poll_interval_seconds) - copy = self._client.get_blob_properties( - self._container_name, dst - ).properties.copy + src_blob_client = self._container_client.get_blob_client(src) + dst_blob_client = self._container_client.get_blob_client(dst) + copy_result = dst_blob_client.start_copy_from_url(src_blob_client.url) + if copy_result.get("copy_status") == "success": + return + + for _ in range(10): + props = dst_blob_client.get_blob_properties() + status = props.copy.status + if status == "success": + return + time.sleep(10) + + if status != "success": + props = dst_blob_client.get_blob_properties() + copy_id = props.copy.id + if copy_id is not None: + dst_blob_client.abort_copy(copy_id) + raise Exception(f"Copy operation failed: {status}") def _rename_file(self, src, dst): self._copy_blob(src, dst) self.delete_file(src) def _rename_directory(self, src, dst): - for blob in self._client.list_blobs(self._container_name, src): + for blob in self._container_client.list_blobs(src): self._rename_file(blob.name, blob.name.replace(src, dst, 1)) def rename_path(self, src, dst): @@ -257,15 +262,21 @@ class AzureFileAdmin(BaseFileAdmin): Azure Blob Storage Connection String Sample usage:: - + from azure.storage.blob import BlobServiceClient from flask_admin import Admin from flask_admin.contrib.fileadmin.azure import AzureFileAdmin admin = Admin() - - admin.add_view(AzureFileAdmin('files_container', 'my-connection-string') + client = BlobServiceClient.from_connection_string("my-connection-string") + admin.add_view(AzureFileAdmin(client, 'files_container') """ - def __init__(self, container_name, connection_string, *args, **kwargs): - storage = AzureStorage(container_name, connection_string) + def __init__( + self, + blob_service_client, + container_name, + *args, + **kwargs, + ): + storage = AzureStorage(blob_service_client, container_name) super().__init__(*args, storage=storage, **kwargs) diff --git a/flask_admin/tests/fileadmin/test_fileadmin_azure.py b/flask_admin/tests/fileadmin/test_fileadmin_azure.py index d88e16b26..bd95fbb72 100644 --- a/flask_admin/tests/fileadmin/test_fileadmin_azure.py +++ b/flask_admin/tests/fileadmin/test_fileadmin_azure.py @@ -1,6 +1,4 @@ -import os.path as op -from os import getenv -from unittest import SkipTest +import os from uuid import uuid4 import pytest @@ -11,30 +9,28 @@ class TestAzureFileAdmin(Base.FileAdminTests): - _test_storage = getenv("AZURE_STORAGE_CONNECTION_STRING") - @pytest.fixture(autouse=True) def setup_and_teardown(self): - if not azure.BlockBlobService: - raise SkipTest("AzureFileAdmin dependencies not installed") - + TEST_STORAGE = os.getenv("AZURE_STORAGE_CONNECTION_STRING") self._container_name = f"fileadmin-tests-{uuid4()}" - if not self._test_storage or not self._container_name: - raise SkipTest("AzureFileAdmin test credentials not set") + if not TEST_STORAGE or not self._container_name: + raise ValueError("AzureFileAdmin test credentials not set, tests will fail") - client = azure.BlockBlobService(connection_string=self._test_storage) - client.create_container(self._container_name) - dummy = op.join(self._test_files_root, "dummy.txt") - client.create_blob_from_path(self._container_name, "dummy.txt", dummy) + self._client = azure.BlobServiceClient.from_connection_string(TEST_STORAGE) + self._client.create_container(self._container_name) + file_name = "dummy.txt" + file_path = os.path.join(self._test_files_root, file_name) + blob_client = self._client.get_blob_client(self._container_name, file_name) + with open(file_path, "rb") as file: + blob_client.upload_blob(file) yield - client = azure.BlockBlobService(connection_string=self._test_storage) - client.delete_container(self._container_name) + self._client.delete_container(self._container_name) def fileadmin_class(self): return azure.AzureFileAdmin def fileadmin_args(self): - return (self._container_name, self._test_storage), {} + return (self._client, self._container_name), {} diff --git a/pyproject.toml b/pyproject.toml index a41508fe4..5bf79e404 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ peewee = [ "wtf-peewee>=3.0.4" ] s3 = ["boto3>=1.33"] -azure-blob-storage = ["azure-storage-blob<=3"] # TODO: update to v12+ +azure-blob-storage = ["azure-storage-blob>=12.0.0"] images = ["pillow>=10.0.0"] export = ["tablib>=3.0.0"] rediscli = ["redis>=4.0.0"] diff --git a/requirements-skip/tests-min.in b/requirements-skip/tests-min.in index d8833da68..9b336a6f8 100644 --- a/requirements-skip/tests-min.in +++ b/requirements-skip/tests-min.in @@ -33,7 +33,7 @@ pymongo==3.7.0 peewee==3.14.0 wtf-peewee==3.0.4 -azure-storage-blob==2.1.0 +azure-storage-blob==12.0.0 pillow==10.0.0 diff --git a/requirements-skip/tests-min.txt b/requirements-skip/tests-min.txt index 267e66593..a4817154d 100644 --- a/requirements-skip/tests-min.txt +++ b/requirements-skip/tests-min.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # pip-compile tests-min.in @@ -8,14 +8,12 @@ arrow==0.13.0 # via -r tests-min.in astroid==3.2.4 # via pylint -azure-common==1.1.28 +azure-core==1.32.0 # via # azure-storage-blob - # azure-storage-common -azure-storage-blob==2.1.0 + # msrest +azure-storage-blob==12.0.0 # via -r tests-min.in -azure-storage-common==2.1.0 - # via azure-storage-blob babel==2.16.0 # via flask-babel beautifulsoup4==4.12.3 @@ -30,7 +28,9 @@ botocore==1.33.13 # moto # s3transfer certifi==2024.8.30 - # via requests + # via + # msrest + # requests cffi==1.17.1 # via cryptography charset-normalizer==3.3.2 @@ -43,7 +43,7 @@ coverage[toml]==7.6.1 # via pytest-cov cryptography==43.0.1 # via - # azure-storage-common + # azure-storage-blob # moto deprecated==1.2.14 # via redis @@ -74,10 +74,12 @@ idna==3.8 # via # email-validator # requests -importlib-metadata==8.4.0 +importlib-metadata==8.5.0 # via flask iniconfig==2.0.0 # via pytest +isodate==0.7.2 + # via msrest isort==5.13.2 # via pylint itsdangerous==2.2.0 @@ -102,8 +104,12 @@ mccabe==0.7.0 # pylint moto==5.0.18 # via -r tests-min.in +msrest==0.7.1 + # via azure-storage-blob numpy==1.24.4 # via shapely +oauthlib==3.2.2 + # via requests-oauthlib packaging==24.1 # via # geoalchemy2 @@ -139,22 +145,23 @@ pytest-cov==5.0.0 python-dateutil==2.9.0.post0 # via # arrow - # azure-storage-common # botocore # moto pytz==2022.7.1 - # via - # babel - # flask-babel + # via flask-babel pyyaml==6.0.2 # via responses redis==4.0.0 # via -r tests-min.in requests==2.32.3 # via - # azure-storage-common + # azure-core # moto + # msrest + # requests-oauthlib # responses +requests-oauthlib==2.0.0 + # via msrest responses==0.25.3 # via moto s3transfer==0.8.2 @@ -163,6 +170,7 @@ shapely==2.0.0 # via -r tests-min.in six==1.16.0 # via + # azure-core # python-dateutil # sqlalchemy-utils soupsieve==2.6 @@ -180,7 +188,7 @@ sqlalchemy-utils==0.38.0 # via -r tests-min.in tablib==3.0.0 # via -r tests-min.in -tomli==2.0.1 +tomli==2.1.0 # via # coverage # pylint @@ -190,6 +198,7 @@ tomlkit==0.13.2 typing-extensions==4.12.2 # via # astroid + # azure-core # pylint urllib3==1.26.20 # via @@ -211,5 +220,5 @@ wtforms==2.3.0 # wtf-peewee xmltodict==0.14.2 # via moto -zipp==3.20.1 +zipp==3.21.0 # via importlib-metadata diff --git a/requirements/dev.txt b/requirements/dev.txt index 8d8ef849d..2a92171ed 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -163,10 +163,6 @@ nodeenv==1.9.1 # -r typing.txt # pre-commit # pyright -numpy==1.24.4 - # via - # -r typing.txt - # types-shapely packaging==24.1 # via # -r docs.txt