diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 349f4908..00000000 --- a/.flake8 +++ /dev/null @@ -1,16 +0,0 @@ -# .flake8 -[flake8] -select = A,B,B9,BLK,C,E,F,I,N,S,W -ignore = E203,W503,E501 -max-complexity = 17 -max-line-length = 120 -application-import-names = datagateway_api,test,util -import-order-style = google -per-file-ignores = - test/*: S101 - util/icat_db_generator.py: S311 - datagateway_api/wsgi.py:E402,F401 - datagateway_api/datagateway_api/icat/models.py: N815,A003 - datagateway_api/datagateway_api/icat/filters.py: C901 - datagateway_api/search_api/models.py: B950 -enable-extensions=G diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index c74e9f8a..22f334d8 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -138,7 +138,7 @@ jobs: version: "0.11.21" - name: Run linting - run: uv run flake8 datagateway_api test util + run: uv run ruff check formatting: runs-on: ubuntu-24.04 @@ -157,7 +157,7 @@ jobs: with: version: "0.11.21" - name: Install dependencies and check formatting - run: uv run black --check datagateway_api test util + run: uv run ruff format generator-script-testing: runs-on: ubuntu-24.04 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f85638d2..56d3bb59 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,8 +11,8 @@ repos: - id: trailing-whitespace - repo: local hooks: - - id: black - name: black - entry: uv run black + - id: formatter + name: ruff + entry: uv run ruff format language: system types: [python] diff --git a/README.md b/README.md index fb83b31e..413e1482 100644 --- a/README.md +++ b/README.md @@ -50,24 +50,23 @@ uv add [PACKAGE-NAME] When developing new features for the API, the following tools are used for code quality and testing: -- [Black](https://black.readthedocs.io/en/stable/) - Code formatting -- [flake8](https://flake8.pycqa.org/en/latest/) - Code linting (with additional plugins configured in `.flake8`) +- [Ruff](https://docs.astral.sh/ruff/) - Code formatting and linting - [pytest](https://docs.pytest.org/en/stable/) - Testing framework All these tools are included as development dependencies in uv and can be run via uv or directly. ### Running Development Tools -Format code with Black: +Format code with ruff: ```bash -uv run black datagateway_api test util +uv run ruff format ``` -Lint code with flake8: +Lint code with ruff: ```bash -uv run flake8 datagateway_api test util +uv run ruff check ``` Run unit tests: diff --git a/datagateway_api/auth/session_bearer.py b/datagateway_api/auth/session_bearer.py index b3763d1f..f4cb4f42 100644 --- a/datagateway_api/auth/session_bearer.py +++ b/datagateway_api/auth/session_bearer.py @@ -1,6 +1,5 @@ import logging - from fastapi import Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer diff --git a/datagateway_api/common/base_query_filter_factory.py b/datagateway_api/common/base_query_filter_factory.py index 147b36ca..803aa45b 100644 --- a/datagateway_api/common/base_query_filter_factory.py +++ b/datagateway_api/common/base_query_filter_factory.py @@ -5,7 +5,7 @@ class QueryFilterFactory(ABC): @abstractstaticmethod def get_query_filter(request_filter, entity_name=None): # noqa: B902, N805 """ - Given a filter, return a matching Query filter object + Given a filter, return a matching Query filter object. :param request_filter: The filter to create the QueryFilter for :type request_filter: :class:`dict` diff --git a/datagateway_api/common/config.py b/datagateway_api/common/config.py index c20c6f61..0a138b22 100644 --- a/datagateway_api/common/config.py +++ b/datagateway_api/common/config.py @@ -1,23 +1,22 @@ -from functools import cached_property import logging -from pathlib import Path import sys -from typing import Annotated, Optional, Self +from functools import cached_property +from pathlib import Path +from typing import Annotated, Self +import yaml from pydantic import ( AfterValidator, BaseModel, - computed_field, Field, - model_validator, SecretStr, StrictBool, StrictInt, StrictStr, ValidationError, + computed_field, + model_validator, ) -import yaml - log = logging.getLogger() @@ -73,7 +72,7 @@ class DataGatewayAPI(BaseModel): extension: DataGatewayAPIExtension icat_check_cert: StrictBool icat_url: StrictStr - use_reader_for_performance: Optional[UseReaderForPerformance] = None + use_reader_for_performance: UseReaderForPerformance | None = None def __getitem__(self, item): return getattr(self, item) @@ -131,14 +130,14 @@ class APIConfig(BaseModel): API startup so any missing options will be caught quickly. """ - datagateway_api: Optional[DataGatewayAPI] = None - reload: Optional[StrictBool] = None - host: Optional[StrictStr] = None - port: Optional[StrictInt] = None - search_api: Optional[SearchAPI] = None - test_mechanism: Optional[StrictStr] = None + datagateway_api: DataGatewayAPI | None = None + reload: StrictBool | None = None + host: StrictStr | None = None + port: StrictInt | None = None + search_api: SearchAPI | None = None + test_mechanism: StrictStr | None = None url_prefix: DataGatewayAPIExtension - test_user_credentials: Optional[TestUserCredentials] = None + test_user_credentials: TestUserCredentials | None = None def __getitem__(self, item): return getattr(self, item) @@ -172,7 +171,7 @@ def load(cls, path=None): ) return cls(**data) - except (IOError, ValidationError) as error: + except (OSError, ValidationError) as error: sys.exit(f"An error occurred while trying to load the config data: {error}") @staticmethod @@ -204,6 +203,6 @@ def _validate_api_extensions(self) -> Self: class Config: - """Class containing config as a class variable so it can mocked during testing""" + """Class containing config as a class variable so it can mocked during testing.""" config = APIConfig.load() diff --git a/datagateway_api/common/date_handler.py b/datagateway_api/common/date_handler.py index cc8d86dd..348d2fa0 100644 --- a/datagateway_api/common/date_handler.py +++ b/datagateway_api/common/date_handler.py @@ -24,7 +24,6 @@ def is_str_a_date(potential_date): :type potential_date: :class:`str` :return: Boolean to signify whether `potential_date` is a date or not """ - text = potential_date.strip() # Reject if the string is just digits (like "5" or "20200101") @@ -58,7 +57,6 @@ def str_to_datetime_object(data): :return: Date converted into a :class:`datetime` object :raises BadRequestError: If there is an issue with the date format """ - try: datetime_obj = helper.parse_attr_string(data, "Date") except ValueError as e: @@ -69,7 +67,7 @@ def str_to_datetime_object(data): @staticmethod def datetime_object_to_str(datetime_obj): """ - Convert a datetime object to a string so it can be outputted in JSON + Convert a datetime object to a string so it can be outputted in JSON. :param datetime_obj: Datetime object from data from an ICAT entity :type datetime_obj: :class:`datetime.datetime` diff --git a/datagateway_api/common/filter_order_handler.py b/datagateway_api/common/filter_order_handler.py index d07dd535..2b01e855 100644 --- a/datagateway_api/common/filter_order_handler.py +++ b/datagateway_api/common/filter_order_handler.py @@ -17,7 +17,7 @@ log = logging.getLogger() -class FilterOrderHandler(object): +class FilterOrderHandler: """ The FilterOrderHandler takes in filters, sorts them according to the order of operations, then applies them. @@ -36,9 +36,7 @@ def remove_filter(self, query_filter): self.filters.remove(query_filter) def sort_filters(self): - """ - Sorts the filters according to the order of operations - """ + """Sorts the filters according to the order of operations.""" self.filters.sort(key=lambda x: x.precedence) def apply_filters(self, query): @@ -84,7 +82,6 @@ def add_icat_relations_for_non_related_fields_of_panosc_related_entities( :param panosc_entity_name: A PaNOSC entity name e.g. "Dataset" :type panosc_entity_name: :class:`str` """ - python_icat_include_filter = None icat_relations = [] for filter_ in self.filters: @@ -120,12 +117,11 @@ def add_icat_relations_for_panosc_non_related_fields( ): """ Retrieve ICAT relations and create a `PythonICATIncludeFilter` for these ICAT - relations + relations. :param panosc_entity_name: A PaNOSC entity name e.g. "Dataset" :type panosc_entity_name: :class:`str` """ - icat_relations = mappings.get_icat_relations_for_panosc_non_related_fields( panosc_entity_name, ) @@ -138,7 +134,7 @@ def add_icat_relations_for_panosc_non_related_fields( def merge_python_icat_limit_skip_filters(self): """ When there are both limit and skip filters in a request, merge them into the - limit filter and remove the skip filter from the instance + limit filter and remove the skip filter from the instance. """ log.info("Merging a PythonICATSkipFilter and PythonICATLimitFilter together") skip_filter = None @@ -177,14 +173,13 @@ def manage_icat_filters(self, filters, query): Utility function to call other functions in this class, used to manage filters when using the Python ICAT. These steps are the same with the different types of requests that utilise filters, therefore this function helps to reduce - code duplication + code duplication. :param filters: The list of filters that will be applied to the query :type filters: List of specific implementations :class:`QueryFilter` :param query: ICAT query which will fetch the data at a later stage :type query: :class:`icat.query.Query` """ - self.add_filters(filters) self.merge_python_icat_limit_skip_filters() self.clear_python_icat_order_filters() diff --git a/datagateway_api/common/filters.py b/datagateway_api/common/filters.py index 1274249d..e84eef8e 100644 --- a/datagateway_api/common/filters.py +++ b/datagateway_api/common/filters.py @@ -1,5 +1,5 @@ -from abc import ABC, abstractmethod import logging +from abc import ABC, abstractmethod from datagateway_api.common.exceptions import BadRequestError, FilterError @@ -20,7 +20,7 @@ def apply_filter(self, query): class WhereFilter(QueryFilter): precedence = 1 - def __init__(self, field, value, operation): + def __init__(self, _field, value, operation): # The field is set to None as a precaution but this should be set # when initialising Python ICAT self.field = None @@ -33,7 +33,7 @@ def __init__(self, field, value, operation): f"When using the {self.operation} operation for a WHERE filter, the" f" values must be in a list format e.g. [1, 2]", ) - if self.operation == "between" and len(self.value) != 2: + if self.operation == "between" and len(self.value) != 2: # noqa: PLR2004 raise BadRequestError( "When using the 'between' operation for a WHERE filter, the list" "must contain two values e.g. [1, 2]", diff --git a/datagateway_api/common/helpers.py b/datagateway_api/common/helpers.py index 38f78b5c..87964887 100644 --- a/datagateway_api/common/helpers.py +++ b/datagateway_api/common/helpers.py @@ -1,12 +1,11 @@ -from datetime import datetime -from functools import wraps import json import logging +from datetime import datetime +from functools import wraps +import requests from fastapi import Request from pydantic import ValidationError -import requests - from datagateway_api.common.date_handler import DateHandler from datagateway_api.common.exceptions import ( @@ -25,7 +24,7 @@ def queries_records(method): Decorator for endpoint resources that search for a record in a table :param method: The method for the endpoint :return: Will return a 404, "No such record" if a MissingRecordError is caught - :return: Will return a 400, "Error message" if other expected errors are caught + :return: Will return a 400, "Error message" if other expected errors are caught. """ @wraps(method) @@ -52,7 +51,7 @@ def get_session_id_from_auth_header(request: Request): """ Gets the sessionID from the Authorization header of a request :request Request: FastAPI Request object containing the incoming headers - :return: String: SessionID + :return: String: SessionID. """ log.info("Getting session Id from auth header") @@ -63,7 +62,7 @@ def get_session_id_from_auth_header(request: Request): auth_header = auth_header_value.split(" ") - if len(auth_header) != 2 or auth_header[0] != "Bearer": + if len(auth_header) != 2 or auth_header[0] != "Bearer": # noqa: PLR2004 raise AuthenticationError( f"Could not authenticate consumer with auth header {auth_header}", ) @@ -75,7 +74,7 @@ def is_valid_json(string): """ Determines if a string is valid JSON :param string: The string to be tested - :return: boolean representing if the string is valid JSON + :return: boolean representing if the string is valid JSON. """ try: json.loads(string) @@ -89,7 +88,7 @@ def is_valid_json(string): def get_filters_from_query_string(request: Request, api_type, entity_name=None): """ Gets a list of filters from the query_strings arg,value pairs, and returns a list of - QueryFilter Objects + QueryFilter Objects. :request Request: FastAPI Request object containing the incoming headers :param api_type: Type of API this function is being used for i.e. DataGateway API or @@ -102,11 +101,11 @@ def get_filters_from_query_string(request: Request, api_type, entity_name=None): :return: The list of filters """ if api_type == "search_api": - from datagateway_api.search_api.query_filter_factory import ( + from datagateway_api.search_api.query_filter_factory import ( # noqa: PLC0415 SearchAPIQueryFilterFactory as QueryFilterFactory, ) elif api_type == "datagateway_api": - from datagateway_api.datagateway_api.query_filter_factory import ( + from datagateway_api.datagateway_api.query_filter_factory import ( # noqa: PLC0415 DataGatewayAPIQueryFilterFactory as QueryFilterFactory, ) else: @@ -135,7 +134,7 @@ def get_icat_properties(icat_url, icat_check_cert): """ ICAT properties can be retrieved using Python ICAT's client object, however this requires the client object to be authenticated which may not always be the case - when requesting these properties, hence a HTTP request is sent as an alternative + when requesting these properties, hence a HTTP request is sent as an alternative. """ properties_url = f"{icat_url}/icat/properties" r = requests.request("GET", properties_url, verify=icat_check_cert) @@ -147,7 +146,7 @@ def get_icat_properties(icat_url, icat_check_cert): def map_distinct_attributes_to_results(distinct_attributes, query_result): """ Maps the attribute names from a distinct filter onto the results given by the result - of a query + of a query. When selecting multiple (but not all) attributes in a database query, the results are returned in a list and not mapped to an entity object. This means the 'normal' @@ -169,7 +168,7 @@ def map_distinct_attributes_to_results(distinct_attributes, query_result): split_attr_name = attr_name.split(".") if isinstance(data, datetime): - data = DateHandler.datetime_object_to_str(data) + data = DateHandler.datetime_object_to_str(data) # noqa: PLW2901 # Attribute name is from the 'origin' entity (i.e. not a related entity) if len(split_attr_name) == 1: @@ -184,7 +183,7 @@ def map_distinct_attributes_to_results(distinct_attributes, query_result): def map_nested_attrs(nested_dict, split_attr_name, query_data): """ A function that can be called recursively to map attributes from related - entities to the associated data + entities to the associated data. :param nested_dict: Dictionary to insert data into :type nested_dict: :class:`dict` diff --git a/datagateway_api/common/logger_setup.py b/datagateway_api/common/logger_setup.py index c2fea3b4..39fe10be 100644 --- a/datagateway_api/common/logger_setup.py +++ b/datagateway_api/common/logger_setup.py @@ -1,6 +1,4 @@ -""" -Module for setting up and configuring the logging system. -""" +"""Module for setting up and configuring the logging system.""" import logging import logging.config @@ -10,7 +8,5 @@ def setup_logger() -> None: - """ - Set up the logger using the configuration INI file. - """ + """Set up the logger using the configuration INI file.""" logging.config.fileConfig(LOGGING_CONFIG_FILE_PATH) diff --git a/datagateway_api/datagateway_api/build_models.py b/datagateway_api/datagateway_api/build_models.py index 7a79ddad..606b385e 100644 --- a/datagateway_api/datagateway_api/build_models.py +++ b/datagateway_api/datagateway_api/build_models.py @@ -1,9 +1,9 @@ import decimal import logging -from typing import Annotated, List, Optional, Union +from typing import Annotated, Optional, Union from icat.exception import ICATError -from pydantic import AwareDatetime, BaseModel, create_model, Field +from pydantic import AwareDatetime, BaseModel, Field, create_model from datagateway_api.common.exceptions import PythonICATError from datagateway_api.datagateway_api.icat.helpers import get_cached_client @@ -47,14 +47,14 @@ class ICATId(BaseModel): - id_: Annotated[Optional[int], Field(None, alias="id")] + id_: Annotated[int | None, Field(None, alias="id")] class ICATBaseEntity(ICATId): - create_id: Annotated[Optional[str], Field(None, alias="createId")] - create_time: Annotated[Optional[AwareDatetime], Field(None, alias="createTime")] - mod_id: Annotated[Optional[str], Field(None, alias="modId")] - mod_time: Annotated[Optional[AwareDatetime], Field(None, alias="modTime")] + create_id: Annotated[str | None, Field(None, alias="createId")] + create_time: Annotated[AwareDatetime | None, Field(None, alias="createTime")] + mod_id: Annotated[str | None, Field(None, alias="modId")] + mod_time: Annotated[AwareDatetime | None, Field(None, alias="modTime")] def build_datagateway_api_model(**kwargs): @@ -109,7 +109,6 @@ def build_datagateway_api_model(**kwargs): - The POST and PATCH models differ by optionality and update semantics. """ - log.info("Building datagateway models") datagateway_api_models = {} @@ -129,13 +128,12 @@ def build_datagateway_api_model(**kwargs): post_name = f"{name}Post" patch_name = f"{name}Patch" for field in info.fields: - if field.name in SYSTEM_FIELDS: continue if field.relType == "ATTRIBUTE": field_type = TYPE_MAP.get(field.type, str) - optional_field_type = Optional[field_type] + optional_field_type = Optional[field_type] # noqa: UP045 description = getattr(field, "comment", None) field_metadata = Field(description=description) @@ -153,7 +151,7 @@ def build_datagateway_api_model(**kwargs): rel_type_str = f"{rel_model_name!r}" post_type = int - optional_type = Optional[post_type] + optional_type = Optional[post_type] # noqa: UP045 rel_type_str = f"Optional[{rel_type_str}]" description = getattr(field, "comment", None) @@ -173,7 +171,7 @@ def build_datagateway_api_model(**kwargs): for model in datagateway_api_models.values(): types_namespace = { **datagateway_api_models, - "List": List, + "List": list, "Optional": Optional, "Union": Union, } diff --git a/datagateway_api/datagateway_api/icat/filters.py b/datagateway_api/datagateway_api/icat/filters.py index 639684bc..686ed9f7 100644 --- a/datagateway_api/datagateway_api/icat/filters.py +++ b/datagateway_api/datagateway_api/icat/filters.py @@ -12,7 +12,6 @@ ) from datagateway_api.common.helpers import get_icat_properties - log = logging.getLogger() @@ -32,7 +31,7 @@ def apply_filter(self, query): def create_filter(self): """ - Create what's needed for a where filter dependent on the operation provided + Create what's needed for a where filter dependent on the operation provided. The logic in this function has been abstracted away from `apply_filter()` to make that function used for its named purpose, and no more. @@ -41,7 +40,6 @@ def create_filter(self): object :raises FilterError: If the operation provided to the instance isn't valid """ - # TODO - need to add support for the rest of search API operators # For things like inq, think we just call this function again with the DG API # version. This will prevent a breaking change from occurring @@ -128,7 +126,7 @@ def create_filter(self): def create_condition(attribute_name, operator, value): """ Construct and return a Python dictionary containing conditions to be used in a - Query object + Query object. :param attribute_name: Attribute name to search :type attribute_name: :class:`str` @@ -139,7 +137,6 @@ def create_condition(attribute_name, operator, value): :return: Condition (of type :class:`dict`) ready to be added to a Python ICAT Query object """ - conditions = {} # Handle unary operators (IS NULL, IS NOT NULL) @@ -173,7 +170,7 @@ def apply_filter(self, query): # These aggregate keywords not currently used in the API, but conditional # present in case they're used in the future - if query.aggregate == "AVG" or query.aggregate == "SUM": + if query.aggregate in {"AVG", "SUM"}: # Distinct can be combined with other aggregate functions query.setAggregate(f"{query.aggregate}:DISTINCT") elif query.aggregate == "COUNT": @@ -273,7 +270,7 @@ def apply_filter(self, query): def icat_set_limit(query, skip_number, limit_number): """ - Add limit (utilising skip and count) to an ICAT query + Add limit (utilising skip and count) to an ICAT query. :param query: ICAT Query object to execute within Python ICAT :type query: :class:`icat.query.Query` @@ -303,7 +300,7 @@ def _extract_filter_fields(self, field): Using recursion, go through the fields and add them to the filter's instance. This means that lists within dictionaries, dictionaries within dictionaries are supported. Where dictionaries are involved, '.' are used to join the fields - together + together. Some (but not all) fields require the plural to be accepted in the include of a Python ICAT query - e.g. 'userGroups' is valid (plural required), but 'dataset' @@ -364,8 +361,7 @@ def _extract_filter_fields(self, field): ) else: raise FilterError( - "Include Filter: Inner field type (inside dictionary) not" - " recognised, cannot interpret input", + "Include Filter: Inner field type (inside dictionary) not recognised, cannot interpret input", ) elif isinstance(field, list): for element in field: diff --git a/datagateway_api/datagateway_api/icat/helpers.py b/datagateway_api/datagateway_api/icat/helpers.py index 21387752..10641b69 100644 --- a/datagateway_api/datagateway_api/icat/helpers.py +++ b/datagateway_api/datagateway_api/icat/helpers.py @@ -1,6 +1,6 @@ +import logging from datetime import datetime, timedelta from functools import wraps -import logging from cachetools import cached from dateutil.tz import tzlocal @@ -82,7 +82,7 @@ def wrapper_requires_session(*args, **kwargs): def get_cached_client(session_id, client_pool): """ Get a client from cache using session ID as the cache parameter (client_pool will - always be given the same object, so won't impact on argument hashing) + always be given the same object, so won't impact on argument hashing). An available client is fetched from the object pool, given a session ID, and kept around in this cache until it becomes 'least recently used'. At this point, the @@ -94,7 +94,6 @@ def get_cached_client(session_id, client_pool): :param client_pool: Client object pool used to fetch an unused client :type client_pool: :class:`ObjectPool` """ - # Get a client from the pool client, stats = client_pool._get_resource() @@ -109,7 +108,7 @@ def get_cached_client(session_id, client_pool): def get_session_details_helper(client): """ - Retrieve details regarding the current session within `client` + Retrieve details regarding the current session within `client`. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -129,7 +128,7 @@ def get_session_details_helper(client): def logout_icat_client(client): """ - Logout a user of the currently authenticated user within `client` + Logout a user of the currently authenticated user within `client`. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -139,7 +138,7 @@ def logout_icat_client(client): def refresh_client_session(client): """ - Refresh the session of the currently authenticated user within `client` + Refresh the session of the currently authenticated user within `client`. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -150,7 +149,7 @@ def refresh_client_session(client): def update_attributes(old_entity, new_entity): """ Updates the attribute(s) of a given object which is a record of an entity from - Python ICAT + Python ICAT. :param old_entity: An existing entity record from Python ICAT :type object: :class:`icat.entities.ENTITY` (implementation of @@ -176,7 +175,6 @@ def update_attributes(old_entity, new_entity): ) from e try: - related_object = new_entity[key] if key != "id": entity_info = old_entity.getAttrInfo(old_entity.client, key) @@ -214,7 +212,7 @@ def get_entity_by_id( return_related_entities=False, ): """ - Gets a record of a given ID from the specified entity + Gets a record of a given ID from the specified entity. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -259,7 +257,7 @@ def get_entity_by_id( def delete_entity_by_id(client, entity_type, id_): """ - Deletes a record of a given ID of the specified entity + Deletes a record of a given ID of the specified entity. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -275,7 +273,7 @@ def delete_entity_by_id(client, entity_type, id_): def update_entity_by_id(client, entity_type, id_, new_data): """ - Gets a record of a given ID of the specified entity + Gets a record of a given ID of the specified entity. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -309,7 +307,7 @@ def update_entity_by_id(client, entity_type, id_, new_data): def get_entity_with_filters(client, entity_type, filters): """ - Gets all the records of a given entity, based on the filters provided in the request + Gets all the records of a given entity, based on the filters provided in the request. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -354,14 +352,13 @@ def get_data_with_filters(client, entity_type, filters, aggregate=None): Gets all the records of a given entity, based on the filters and an optional aggregate provided in the request. This function is called by `get_entity_with_filters()` and `get_count_with_filters()` that deal with GET entity - and GET /count entity endpoints respectively + and GET /count entity endpoints respectively. This function uses the reader performance query functionality IF it is enabled in the config. Checks are done to see whether this functionality has been enabled and whether the query is suitable to be completed with the reader account. There are more details about the inner workings in ReaderQueryHandler """ - if is_use_reader_for_performance_enabled(): # See if this query is eligible to benefit from running faster using the reader account reader_query = ReaderQueryHandler(entity_type, filters) @@ -395,9 +392,8 @@ def get_data_with_filters(client, entity_type, filters, aggregate=None): def execute_entity_query(client, entity_type, filters, aggregate=None): """ Assemble a query object with the user's query filters and execute the query by - passing it to ICAT, returning them in this function + passing it to ICAT, returning them in this function. """ - query = ICATQuery(client, entity_type, aggregate=aggregate) filter_handler = FilterOrderHandler() @@ -415,21 +411,18 @@ def execute_entity_query(client, entity_type, filters, aggregate=None): def is_use_reader_for_performance_enabled() -> bool: """ Returns true is the 'use_reader_for_performance' section is present in the - config file and 'enabled' in that section is set to true + config file and 'enabled' in that section is set to true. """ reader_config = Config.config.datagateway_api.use_reader_for_performance if not reader_config: return False - if not reader_config.enabled: - return False - - return True + return reader_config.enabled def get_first_result_with_filters(client, entity_type, filters): """ Using filters in the request, get results of the given entity, but only show the - first one to the user + first one to the user. Since only one result will be outputted, inserting a `PythonICATLimitFilter` in the query will make Python ICAT's data fetching more snappy and prevent a 500 being @@ -464,7 +457,7 @@ def get_first_result_with_filters(client, entity_type, filters): def update_entities(client, entity_type, data_to_update): """ Update one or more results for the given entity using the JSON provided in - `data_to_update` + `data_to_update`. If an exception occurs while sending data to icatdb, an attempt will be made to restore a backup of the data made before making the update. @@ -502,7 +495,8 @@ def update_entities(client, entity_type, data_to_update): updated_icat_data.append(updated_entity_data) except KeyError as e: raise BadRequestError( - "The new data in the request body must contain the ID (using the key: 'id') of the entity you wish to update", + "The new data in the request body must contain the ID (using the key: 'id') of the entity you wish to " + "update", ) from e # This separates the local data updates from pushing these updates to icatdb @@ -531,7 +525,7 @@ def update_entities(client, entity_type, data_to_update): def create_entities(client, entity_type, data): # noqa: C901 """ - Add one or more results for the given entity using the JSON provided in `data` + Add one or more results for the given entity using the JSON provided in `data`. `created_icat_data` is data of `icat.entity.Entity` type that is collated to be pushed to ICAT at the end of the function - this avoids confusion over which data @@ -557,7 +551,6 @@ def create_entities(client, entity_type, data): # noqa: C901 data = [data] for result in data: - new_entity = client.new(entity_type.lower()) for attribute_name, value in result.items(): @@ -571,13 +564,12 @@ def create_entities(client, entity_type, data): # noqa: C901 # Short circuiting ensures is_str_date() will only be executed if # value is a string if isinstance(value, str) and DateHandler.is_str_a_date(value): - value = DateHandler.str_to_datetime_object(value) + value = DateHandler.str_to_datetime_object(value) # noqa: PLW2901 setattr(new_entity, attribute_name, value) else: # This means the attribute has a relationship with another object try: - related_object = [] if entity_info.relType.lower() == "many": related_object = build_related_entities( @@ -631,10 +623,7 @@ def build_related_entities( value: list | None, parent_entity_type: str | None = None, ): - """ - Build related ICAT entities recursively, preserving cascade logic. - """ - + """Build related ICAT entities recursively, preserving cascade logic.""" if not value: return [] related_objects = [] diff --git a/datagateway_api/datagateway_api/icat/icat_client_pool.py b/datagateway_api/datagateway_api/icat/icat_client_pool.py index ec330b86..db57331d 100644 --- a/datagateway_api/datagateway_api/icat/icat_client_pool.py +++ b/datagateway_api/datagateway_api/icat/icat_client_pool.py @@ -9,7 +9,7 @@ class ICATClient(Client): - """Wrapper class to allow an object pool of client objects to be created""" + """Wrapper class to allow an object pool of client objects to be created.""" def __init__(self, client_use="datagateway_api"): if client_use == "datagateway_api": @@ -27,18 +27,17 @@ def __init__(self, client_use="datagateway_api"): def clean_up(self): """ Allows object pool to cleanup the client's resources, using the existing Python - ICAT functionality + ICAT functionality. """ super().cleanup() def create_client_pool(): """ - Function to create an object pool for ICAT client objects + Function to create an object pool for ICAT client objects. The ObjectPool class uses the singleton design pattern """ - return ObjectPool( ICATClient, min_init=Config.config.datagateway_api.client_pool_init_size, diff --git a/datagateway_api/datagateway_api/icat/lru_cache.py b/datagateway_api/datagateway_api/icat/lru_cache.py index 8b584cae..7e9104ac 100644 --- a/datagateway_api/datagateway_api/icat/lru_cache.py +++ b/datagateway_api/datagateway_api/icat/lru_cache.py @@ -10,7 +10,7 @@ class ExtendedLRUCache(LRUCache): """ An extension to cachetools' LRUCache class to allow client objects to be pushed back - into the pool + into the pool. This version of LRU cache was chosen instead of the builtin LRU cache as it allows for addtional actions to be added when an item leaves the cache (controlled by diff --git a/datagateway_api/datagateway_api/icat/models.py b/datagateway_api/datagateway_api/icat/models.py index 95b27d69..07eead38 100644 --- a/datagateway_api/datagateway_api/icat/models.py +++ b/datagateway_api/datagateway_api/icat/models.py @@ -1,10 +1,9 @@ from datetime import datetime -from typing import Optional from pydantic import BaseModel class Session(BaseModel): id: str - expireDateTime: Optional[datetime] = None - username: Optional[str] = None + expireDateTime: datetime | None = None # noqa: N815 + username: str | None = None diff --git a/datagateway_api/datagateway_api/icat/python_icat.py b/datagateway_api/datagateway_api/icat/python_icat.py index f312cf4d..6fd11aaf 100644 --- a/datagateway_api/datagateway_api/icat/python_icat.py +++ b/datagateway_api/datagateway_api/icat/python_icat.py @@ -21,14 +21,11 @@ update_entity_by_id, ) - log = logging.getLogger() class PythonICAT: - """ - Class that contains functions to access and modify data in an ICAT database directly - """ + """Class that contains functions to access and modify data in an ICAT database directly.""" def ping(self, **kwargs): """ @@ -102,7 +99,7 @@ def refresh(self, session_id, **kwargs): @requires_session_id @queries_records - def logout(self, session_id, **kwargs): + def logout(self, _session_id, **kwargs): """ Logs a user out. :param session_id: The user's session ID. @@ -112,7 +109,7 @@ def logout(self, session_id, **kwargs): @requires_session_id @queries_records - def get_with_filters(self, session_id, entity_type, filters, **kwargs): + def get_with_filters(self, _session_id, entity_type, filters, **kwargs): """ Given a list of filters supplied in JSON format, returns entities that match the filters for the given entity type. @@ -125,7 +122,7 @@ def get_with_filters(self, session_id, entity_type, filters, **kwargs): @requires_session_id @queries_records - def create(self, session_id, entity_type, data, **kwargs): + def create(self, _session_id, entity_type, data, **kwargs): """ Create one or more entities, from the given list containing JSON. Each entity must not contain its ID. @@ -138,7 +135,7 @@ def create(self, session_id, entity_type, data, **kwargs): @requires_session_id @queries_records - def update(self, session_id, entity_type, data, **kwargs): + def update(self, _session_id, entity_type, data, **kwargs): """ Update one or more entities, from the given list containing JSON. Each entity must contain its ID. @@ -151,7 +148,7 @@ def update(self, session_id, entity_type, data, **kwargs): @requires_session_id @queries_records - def get_one_with_filters(self, session_id, entity_type, filters, **kwargs): + def get_one_with_filters(self, _session_id, entity_type, filters, **kwargs): """ Returns the first entity that matches a given filter, for a given entity type. :param session_id: The session ID of the requesting user. @@ -163,7 +160,7 @@ def get_one_with_filters(self, session_id, entity_type, filters, **kwargs): @requires_session_id @queries_records - def count_with_filters(self, session_id, entity_type, filters, **kwargs): + def count_with_filters(self, _session_id, entity_type, filters, **kwargs): """ Returns the count of the entities that match a given filter for a given entity type. @@ -176,7 +173,7 @@ def count_with_filters(self, session_id, entity_type, filters, **kwargs): @requires_session_id @queries_records - def get_with_id(self, session_id, entity_type, id_, **kwargs): + def get_with_id(self, _session_id, entity_type, id_, **kwargs): """ Gets the entity matching the given ID for the given entity type. :param session_id: The session ID of the requesting user. @@ -188,7 +185,7 @@ def get_with_id(self, session_id, entity_type, id_, **kwargs): @requires_session_id @queries_records - def delete_with_id(self, session_id, entity_type, id_, **kwargs): + def delete_with_id(self, _session_id, entity_type, id_, **kwargs): """ Deletes the row matching the given ID for the given entity type. :param session_id: The session ID of the requesting user. @@ -199,7 +196,7 @@ def delete_with_id(self, session_id, entity_type, id_, **kwargs): @requires_session_id @queries_records - def update_with_id(self, session_id, entity_type, id_, data, **kwargs): + def update_with_id(self, _session_id, entity_type, id_, data, **kwargs): """ Updates the row matching the given ID for the given entity type. :param session_id: The session ID of the requesting user. diff --git a/datagateway_api/datagateway_api/icat/query.py b/datagateway_api/datagateway_api/icat/query.py index a0aebb76..a22f9bc6 100644 --- a/datagateway_api/datagateway_api/icat/query.py +++ b/datagateway_api/datagateway_api/icat/query.py @@ -1,5 +1,5 @@ -from datetime import datetime import logging +from datetime import datetime from icat.entity import Entity, EntityList from icat.exception import ICATInternalError, ICATValidationError @@ -9,7 +9,6 @@ from datagateway_api.common.exceptions import PythonICATError from datagateway_api.common.helpers import map_distinct_attributes_to_results - log = logging.getLogger() @@ -23,7 +22,7 @@ def __init__( includes=None, ): """ - Create a Query object within Python ICAT + Create a Query object within Python ICAT. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -42,7 +41,6 @@ def __init__( :raises PythonICATError: If a ValueError is raised when creating a Query(), 500 will be returned as a response """ - try: log.info("Creating ICATQuery for entity: %s", entity_name) self.query = Query( @@ -62,7 +60,7 @@ def __init__( def execute_query(self, client, return_json_formattable=False): """ Execute the ICAT Query object and return in the format specified by the - return_json_formattable flag + return_json_formattable flag. :param client: ICAT client containing an authenticated user :type client: :class:`icat.client.Client` @@ -75,7 +73,6 @@ def execute_query(self, client, return_json_formattable=False): :return: Data (of type list) from the executed query :raises PythonICATError: If an error occurs during query execution """ - try: log.debug("Executing ICAT query: %s", self.query) query_result = client.search(self.query) @@ -87,10 +84,9 @@ def execute_query(self, client, return_json_formattable=False): # If the query has a COUNT function applied to it, some of these steps can be # skipped count_query = False - if self.query.aggregate is not None: - if "COUNT" in self.query.aggregate: - count_query = True - log.debug("This ICATQuery is used for COUNT purposes") + if self.query.aggregate is not None and "COUNT" in self.query.aggregate: + count_query = True + log.debug("This ICATQuery is used for COUNT purposes") distinct_query = False if self.query.aggregate == "DISTINCT" and not count_query and not self.query.manual_count: @@ -116,7 +112,7 @@ def execute_query(self, client, return_json_formattable=False): # list as `map_distinct_attributes_to_results()` assumes a list as # input if not isinstance(result, tuple): - result = [result] + result = [result] # noqa: PLW2901 # Map distinct attributes and result data.append( @@ -139,7 +135,7 @@ def get_distinct_attributes(self): def entity_to_dict(self, entity, includes): """ This expands on Python ICAT's implementation of `icat.entity.Entity.as_dict()` - to use set operators to create a version of the entity as a dictionary + to use set operators to create a version of the entity as a dictionary. Most of this function is dedicated to recursing over included fields from a query, since this is functionality isn't part of Python ICAT's `as_dict()`. This @@ -156,14 +152,12 @@ def entity_to_dict(self, entity, includes): :type includes: :class:`list` :return: ICAT Data (of type dictionary) ready to be serialised to JSON """ - d = {} # Verifying that `includes` only has fields which are related to the entity include_set = (entity.InstRel | entity.InstMRel) & set(includes) for key in entity.InstAttr | entity.MetaAttr | include_set: if key in includes: - target = getattr(entity, key) # Copy and remove don't return values so must be done separately includes_copy = includes.copy() @@ -196,7 +190,7 @@ def entity_to_dict(self, entity, includes): def flatten_query_included_fields(self, includes): """ This will take the set of fields included in an ICAT query, split up the fields - separated by dots, and flatten the resulting list + separated by dots, and flatten the resulting list. :param includes: Set of fields that have been included in the ICAT query. Where fields have a chain of relationships, they're a single element string @@ -205,5 +199,4 @@ def flatten_query_included_fields(self, includes): :return: Flattened list containing all the fields that have been included in the ICAT query """ - return [m for n in (field.split(".") for field in sorted(includes)) for m in n] diff --git a/datagateway_api/datagateway_api/icat/reader_query_handler.py b/datagateway_api/datagateway_api/icat/reader_query_handler.py index ba0dee48..b7ed27e6 100644 --- a/datagateway_api/datagateway_api/icat/reader_query_handler.py +++ b/datagateway_api/datagateway_api/icat/reader_query_handler.py @@ -1,6 +1,5 @@ -from datetime import datetime, timezone import logging -from typing import List, Optional +from datetime import UTC, datetime from cachetools.func import ttl_cache from icat.exception import ICATSessionError @@ -51,7 +50,7 @@ class ReaderQueryHandler: maxsize = Config.config.datagateway_api.use_reader_for_performance.maxsize ttl = Config.config.datagateway_api.use_reader_for_performance.ttl - def __init__(self, entity_type: str, filters: List[QueryFilter]) -> None: + def __init__(self, entity_type: str, filters: list[QueryFilter]) -> None: self.entity_type = entity_type self.filters = filters log.debug( @@ -67,7 +66,7 @@ def create_reader_client(cls) -> ICATClient: """ Create a new client (assigning it as a class variable) and login using the reader's credentials. If the credentials aren't valid, a PythonICATError is - raised (resulting in a 500). The client object is returned + raised (resulting in a 500). The client object is returned. """ log.info("Creating reader_client") cls.reader_client = ICATClient("datagateway_api") @@ -114,6 +113,7 @@ def get_investigation_id(cls, dataset_id: int) -> int: Returns: int: ICAT id of the Dataset's parent Investigation. + """ query = f"SELECT d.investigation.id FROM Dataset d WHERE d.id={dataset_id}" # noqa: S608 cls.refresh() @@ -134,10 +134,9 @@ def get_investigation_users(cls, investigation_id: int) -> set[str]: Returns: set[str]: ICAT User.name of all InvestigationUsers associated with the Investigation with id `investigation_id`. + """ - query = ( - f"SELECT iu.user.name FROM InvestigationUser iu WHERE iu.investigation.id={investigation_id}" # noqa: S608 - ) + query = f"SELECT iu.user.name FROM InvestigationUser iu WHERE iu.investigation.id={investigation_id}" # noqa: S608 cls.refresh() user_names = cls.reader_client.search(query=query) log.debug("Found %s as InvestigationUsers for investigation.id=%s", user_names, investigation_id) @@ -153,6 +152,7 @@ def get_instrument_scientists(cls, investigation_id: int) -> set[str]: Returns: set[str]: ICAT User.name of all InstrumentScientists associated with the Investigation with id `investigation_id`. + """ query = ( "SELECT s.user.name FROM InstrumentScientist s " # noqa: S608 @@ -172,6 +172,7 @@ def is_user_allowed(cls, user_name: str, investigation_id: int) -> bool: Returns: bool: If `user_name` has an association with `investigation_id` allowing read access. + """ return user_name in cls.get_investigation_users( investigation_id=investigation_id, @@ -186,6 +187,7 @@ def is_dataset_open(cls, dataset_id: int) -> bool: Returns: bool: Whether the Dataset with `dataset_id` has been made open/public. + """ query = ( "SELECT dp.publicationDate FROM DataPublication dp JOIN dp.content c " # noqa: S608 @@ -193,7 +195,7 @@ def is_dataset_open(cls, dataset_id: int) -> bool: ) cls.refresh() for publication_date in cls.reader_client.search(query): - if publication_date < datetime.now(tz=timezone.utc): + if publication_date < datetime.now(tz=UTC): log.debug("dataset.id=%s is open", dataset_id) return True @@ -205,28 +207,24 @@ def check_eligibility(self) -> bool: This function checks whether the input query can be executed as a 'reader performance query'. The entity of the query needs to be in `entity_filter_check` and an appropriate WHERE filter needs to be sought - (using `get_where_filter_for_entity_id_check()`) + (using `get_where_filter_for_entity_id_check()`). """ log.info("Checking whether query is eligible to go via reader account") - if self.entity_type in ReaderQueryHandler.entity_filter_check.keys(): - if self.get_where_filter_for_entity_id_check(): - return True - return False + return self.entity_type in ReaderQueryHandler.entity_filter_check and bool( + self.get_where_filter_for_entity_id_check() + ) def is_query_eligible_for_reader_performance(self) -> bool: - """ - Getter that returns a boolean regarding query eligibility - """ + """Getter that returns a boolean regarding query eligibility.""" return self.reader_query_eligible - def get_where_filter_for_entity_id_check(self) -> Optional[PythonICATWhereFilter]: + def get_where_filter_for_entity_id_check(self) -> PythonICATWhereFilter | None: """ Iterate through the instance's query filters and return a WHERE filter for a relevant parent entity (e.g. dataset.id or datafile.id). The WHERE filter must - use the 'eq' operator + use the 'eq' operator. """ - for query_filter in self.filters: if ( isinstance(query_filter, PythonICATWhereFilter) @@ -247,7 +245,7 @@ def is_user_authorised_to_see_entity_id(self, client) -> bool: This function checks whether the user is authorised to see a parent entity (e.g. if they query /datafiles, whether they can see a particular dataset). A query is created and sent to ICAT for execution - the query is performed using the - session ID provided in the request + session ID provided in the request. """ user_name = client.getUserName() id_field = ReaderQueryHandler.entity_filter_check[self.entity_type] diff --git a/datagateway_api/datagateway_api/query_filter_factory.py b/datagateway_api/datagateway_api/query_filter_factory.py index 0d21c8ca..b2b2b28f 100644 --- a/datagateway_api/datagateway_api/query_filter_factory.py +++ b/datagateway_api/datagateway_api/query_filter_factory.py @@ -4,10 +4,20 @@ from datagateway_api.common.exceptions import FilterError from datagateway_api.datagateway_api.icat.filters import ( PythonICATDistinctFieldFilter as DistinctFieldFilter, +) +from datagateway_api.datagateway_api.icat.filters import ( PythonICATIncludeFilter as IncludeFilter, +) +from datagateway_api.datagateway_api.icat.filters import ( PythonICATLimitFilter as LimitFilter, +) +from datagateway_api.datagateway_api.icat.filters import ( PythonICATOrderFilter as OrderFilter, +) +from datagateway_api.datagateway_api.icat.filters import ( PythonICATSkipFilter as SkipFilter, +) +from datagateway_api.datagateway_api.icat.filters import ( PythonICATWhereFilter as WhereFilter, ) @@ -16,9 +26,9 @@ class DataGatewayAPIQueryFilterFactory(QueryFilterFactory): @staticmethod - def get_query_filter(request_filter, entity_name=None): + def get_query_filter(request_filter, _entity_name=None): """ - Given a filter, return a matching Query filter object + Given a filter, return a matching Query filter object. :param request_filter: The filter to create the QueryFilter for :type request_filter: :class:`dict` @@ -30,7 +40,6 @@ def get_query_filter(request_filter, entity_name=None): :return: The QueryFilter object created :raises FilterError: If the filter name is not recognised """ - filter_name = list(request_filter)[0].lower() if filter_name == "where": field = list(request_filter[filter_name].keys())[0] diff --git a/datagateway_api/datagateway_api/routers/entity.py b/datagateway_api/datagateway_api/routers/entity.py index 2ffd2f95..4bcfd0c7 100644 --- a/datagateway_api/datagateway_api/routers/entity.py +++ b/datagateway_api/datagateway_api/routers/entity.py @@ -1,5 +1,4 @@ -from typing import Annotated, Any, List, Type - +from typing import Annotated, Any from fastapi import APIRouter, Path, Query, Request from pydantic import BaseModel, Json @@ -172,7 +171,7 @@ def get_endpoint( router: APIRouter, endpoint_name: str, entity_name: str, - dg_models: dict[str, Type[BaseModel]], + dg_models: dict[str, type[BaseModel]], python_icat: PythonICAT, **kwargs, ) -> None: @@ -194,7 +193,7 @@ def get_endpoint( "", summary=f"Get {endpoint_name}", description=f"Retrieves a list of {entity_name} objects", - response_model=List[dg_models[entity_name]], + response_model=list[dg_models[entity_name]], response_model_exclude_unset=True, responses={ 200: {"description": f"Success - returns {entity_name} that satisfy the filters"}, @@ -206,12 +205,12 @@ def get_endpoint( ) def get( request: Request, - where: List[Json] = WhereQuery, # pylint:disable=unused-argument - order: List[str] = OrderQuery, # pylint:disable=unused-argument - limit: int = LimitQuery, # pylint:disable=unused-argument - skip: int = SkipQuery, # pylint:disable=unused-argument - distinct: List[str] = DistinctQuery, # pylint:disable=unused-argument - include: Any = IncludeQuery, # pylint:disable=unused-argument + where: list[Json] = WhereQuery, # noqa: ARG001 + order: list[str] = OrderQuery, # noqa: ARG001 + limit: int = LimitQuery, # noqa: ARG001 + skip: int = SkipQuery, # noqa: ARG001 + distinct: list[str] = DistinctQuery, # noqa: ARG001 + include: Any = IncludeQuery, # noqa: ARG001 ): return python_icat.get_with_filters( get_session_id_from_auth_header(request), @@ -224,7 +223,7 @@ def get( "", summary=f"Create new {endpoint_name}", description=(f"Creates new {entity_name} object(s) with details provided in the request body"), - response_model=List[dg_models[entity_name]], + response_model=list[dg_models[entity_name]], response_model_exclude_unset=True, responses={ 200: {"description": "Success - returns the created object"}, @@ -234,7 +233,7 @@ def get( 404: {"description": "No such record - Unable to find a record in ICAT"}, }, ) - def post(body: List[dg_models[f"{entity_name}Post"]], request: Request): # noqa: F821 + def post(body: list[dg_models[f"{entity_name}Post"]], request: Request): # noqa: F821 return python_icat.create( get_session_id_from_auth_header(request), entity_name, @@ -246,7 +245,7 @@ def post(body: List[dg_models[f"{entity_name}Post"]], request: Request): # noqa "", summary=f"Update {endpoint_name}", description=(f"Updates {entity_name} object(s) with details provided in the request body"), - response_model=List[dg_models[entity_name]], + response_model=list[dg_models[entity_name]], response_model_exclude_unset=True, responses={ 200: {"description": "Success - returns the updated object(s)"}, @@ -256,7 +255,7 @@ def post(body: List[dg_models[f"{entity_name}Post"]], request: Request): # noqa 404: {"description": "No such record - Unable to find a record in ICAT"}, }, ) - def patch(body: List[dg_models[f"{entity_name}Patch"]], request: Request): # noqa: F821 + def patch(body: list[dg_models[f"{entity_name}Patch"]], request: Request): # noqa: F821 return python_icat.update( get_session_id_from_auth_header(request), entity_name, @@ -269,7 +268,7 @@ def get_id_endpoint( router: APIRouter, endpoint_name: str, entity_name: str, - dg_models: dict[str, Type[BaseModel]], + dg_models: dict[str, type[BaseModel]], python_icat: PythonICAT, **kwargs, ) -> None: @@ -409,9 +408,9 @@ def get_count_endpoint( ) def get( request: Request, - where: List[Json] = WhereQuery, # pylint:disable=unused-argument - distinct: List[str] = DistinctQuery, # pylint:disable=unused-argument - include: Any = IncludeQuery, # pylint:disable=unused-argument + where: list[Json] = WhereQuery, # noqa: ARG001 + distinct: list[str] = DistinctQuery, # noqa: ARG001 + include: Any = IncludeQuery, # noqa: ARG001 ): filters = get_filters_from_query_string(request, "datagateway_api") @@ -426,7 +425,7 @@ def get( def get_find_one_endpoint( router: APIRouter, entity_name: str, - dg_models: dict[str, Type[BaseModel]], + dg_models: dict[str, type[BaseModel]], python_icat: PythonICAT, **kwargs, ) -> None: @@ -459,12 +458,12 @@ def get_find_one_endpoint( ) def get( request: Request, - where: List[Json] = WhereQuery, # pylint:disable=unused-argument - order: List[str] = OrderQuery, # pylint:disable=unused-argument - limit: int = LimitQuery, # pylint:disable=unused-argument - skip: int = SkipQuery, # pylint:disable=unused-argument - distinct: List[str] = DistinctQuery, # pylint:disable=unused-argument - include: Any = IncludeQuery, # pylint:disable=unused-argument + where: list[Json] = WhereQuery, # noqa: ARG001 + order: list[str] = OrderQuery, # noqa: ARG001 + limit: int = LimitQuery, # noqa: ARG001 + skip: int = SkipQuery, # noqa: ARG001 + distinct: list[str] = DistinctQuery, # noqa: ARG001 + include: Any = IncludeQuery, # noqa: ARG001 ): filters = get_filters_from_query_string(request, "datagateway_api") @@ -479,7 +478,7 @@ def get( def create_collection_router( endpoint_name: str, entity_name: str, - dg_models: dict[str, Type[BaseModel]], + dg_models: dict[str, type[BaseModel]], python_icat: PythonICAT, **kwargs, ) -> APIRouter: diff --git a/datagateway_api/datagateway_api/routers/ping.py b/datagateway_api/datagateway_api/routers/ping.py index 4be6f8a5..07dd7d9f 100644 --- a/datagateway_api/datagateway_api/routers/ping.py +++ b/datagateway_api/datagateway_api/routers/ping.py @@ -9,13 +9,12 @@ def ping_endpoint(python_icat, **kwargs) -> APIRouter: """ Generate a FastAPI router using python ICAT. In main.py these routers are included e.g. - `app.include_router(ping_endpoint(python_icat), prefix="/ping")` + `app.include_router(ping_endpoint(python_icat), prefix="/ping")`. :param python_icat: The python ICAT instance used for processing requests :type python_icat: PythonICAT :return: FastAPI APIRouter """ - router = APIRouter(prefix="/ping", tags=["Ping"]) @router.get( diff --git a/datagateway_api/datagateway_api/routers/sessions.py b/datagateway_api/datagateway_api/routers/sessions.py index 994a0a0d..4a2adab9 100644 --- a/datagateway_api/datagateway_api/routers/sessions.py +++ b/datagateway_api/datagateway_api/routers/sessions.py @@ -1,6 +1,6 @@ -from datetime import datetime import logging -from typing import Annotated, Literal, Optional +from datetime import datetime +from typing import Annotated, Literal from fastapi import APIRouter, Depends, Request, status from pydantic import BaseModel, ConfigDict, Field @@ -14,7 +14,7 @@ class LoginRequest(BaseModel): username: str password: str - mechanism: Optional[str] = "simple" + mechanism: str | None = "simple" class SessionResponse(BaseModel): @@ -33,13 +33,12 @@ def sessions_endpoints(python_icat, **kwargs) -> APIRouter: """ Generate a FastAPI APIRouter using python ICAT. In main.py these generated routers are included with the API e.g. - `app.include_router(session_endpoints(python_icat), prefix="/sessions")` + `app.include_router(session_endpoints(python_icat), prefix="/sessions")`. :param python_icat: The python ICAT instance used for processing requests :type python_icat: :class:`PythonICAT` :return: FastAPI APIRouter """ - router = APIRouter(prefix="/sessions", tags=["Sessions"]) session_auth = SessionBearer() @@ -61,7 +60,7 @@ def sessions_endpoints(python_icat, **kwargs) -> APIRouter: def post(request: LoginRequest): """ Generates a sessionID if the user has correct credentials - :return: String - SessionID + :return: String - SessionID. """ return {"sessionID": python_icat.login(request.model_dump(), **kwargs)} @@ -81,9 +80,8 @@ def post(request: LoginRequest): def delete(request: Request, _: Annotated[str, Depends(session_auth)]): """ Deletes a users sessionID when they logout - :return: Blank response, 200 + :return: Blank response, 200. """ - python_icat.logout(get_session_id_from_auth_header(request), **kwargs) return "" @@ -112,7 +110,7 @@ def delete(request: Request, _: Annotated[str, Depends(session_auth)]): def get(request: Request, _: Annotated[str, Depends(session_auth)]): """ Gives details of a user's session - :return: Session details + :return: Session details. """ return python_icat.get_session_details(get_session_id_from_auth_header(request), **kwargs) @@ -132,9 +130,8 @@ def get(request: Request, _: Annotated[str, Depends(session_auth)]): def put(request: Request, _: Annotated[str, Depends(session_auth)]): """ Refreshes a user's session - :return: The session ID that has been refreshed + :return: The session ID that has been refreshed. """ - python_icat.refresh(get_session_id_from_auth_header(request), **kwargs) return "" diff --git a/datagateway_api/main.py b/datagateway_api/main.py index 54df036f..6c2caeca 100644 --- a/datagateway_api/main.py +++ b/datagateway_api/main.py @@ -1,10 +1,9 @@ import logging - +import uvicorn from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse -import uvicorn from datagateway_api.common.config import Config from datagateway_api.common.exceptions import ApiError @@ -15,18 +14,18 @@ search_api_enabled = Config.config.search_api is not None if datagateway_api_enabled: + from datagateway_api.auth.session_bearer import SessionBearer + from datagateway_api.common.entity_endpoint_dict import endpoints from datagateway_api.datagateway_api.build_models import build_datagateway_api_model from datagateway_api.datagateway_api.icat.icat_client_pool import create_client_pool from datagateway_api.datagateway_api.icat.python_icat import PythonICAT from datagateway_api.datagateway_api.routers.entity import create_collection_router from datagateway_api.datagateway_api.routers.ping import ping_endpoint from datagateway_api.datagateway_api.routers.sessions import sessions_endpoints - from datagateway_api.auth.session_bearer import SessionBearer - from datagateway_api.common.entity_endpoint_dict import endpoints if search_api_enabled: - from datagateway_api.search_api.routers.entity import create_search_collection_router from datagateway_api.common.search_api_entity_endpoint_dict import search_api_entity_endpoints + from datagateway_api.search_api.routers.entity import create_search_collection_router setup_logger() logger = logging.getLogger() @@ -48,9 +47,7 @@ async def custom_api_error_handler(_: Request, exc: ApiError) -> JSONResponse: # catch-all for unexpected exceptions async def custom_general_exception_handler(_: Request, exc: Exception) -> JSONResponse: - """ - Handles all uncaught exceptions to prevent internal server errors from leaking. - """ + """Handles all uncaught exceptions to prevent internal server errors from leaking.""" logger.exception(exc) return JSONResponse( status_code=500, diff --git a/datagateway_api/search_api/condition_setting_query.py b/datagateway_api/search_api/condition_setting_query.py index efdf779a..90f91d71 100644 --- a/datagateway_api/search_api/condition_setting_query.py +++ b/datagateway_api/search_api/condition_setting_query.py @@ -4,10 +4,10 @@ class ConditionSettingQuery(Query): """ Custom Query class to support the getting and setting of WHERE clauses outside of - the typical `Query.conditions` dict + the typical `Query.conditions` dict. """ - def __init__( + def __init__( # noqa: PLR0913 self, client, entity_name, @@ -33,9 +33,8 @@ def setConditionsByString(self, str_conditions): # noqa: N802 def where_clause(self): """ Overriding Python ICAT's implementation to support the creation of WHERE clauses - within the search API + within the search API. """ - if self._str_conditions: return f"WHERE {self._str_conditions}" else: diff --git a/datagateway_api/search_api/filters.py b/datagateway_api/search_api/filters.py index 25fac890..e2d1e98d 100644 --- a/datagateway_api/search_api/filters.py +++ b/datagateway_api/search_api/filters.py @@ -1,6 +1,6 @@ +import logging from copy import copy from datetime import datetime -import logging from datagateway_api.common.date_handler import DateHandler from datagateway_api.datagateway_api.icat.filters import ( @@ -109,9 +109,8 @@ def apply_filter(self, query): def __str__(self): """ String representation which is also used to apply WHERE filters that are inside - a `NestedWhereFilters` object + a `NestedWhereFilters` object. """ - if isinstance(self.search_api_query, SearchAPIQuery): # Making a copy of the filter because `apply_filter()` can only be executed # once per filter successfully diff --git a/datagateway_api/search_api/helpers.py b/datagateway_api/search_api/helpers.py index c68fadf9..02ef3216 100644 --- a/datagateway_api/search_api/helpers.py +++ b/datagateway_api/search_api/helpers.py @@ -1,6 +1,6 @@ -from functools import wraps import json import logging +from functools import wraps from pydantic import ValidationError @@ -11,18 +11,17 @@ SearchAPIError, ) from datagateway_api.common.filter_order_handler import FilterOrderHandler +from datagateway_api.search_api import models from datagateway_api.search_api.filters import ( SearchAPIIncludeFilter, SearchAPIWhereFilter, ) -import datagateway_api.search_api.models as models from datagateway_api.search_api.query import SearchAPIQuery from datagateway_api.search_api.session_handler import ( - client_manager, SessionHandler, + client_manager, ) - log = logging.getLogger() @@ -30,7 +29,7 @@ def search_api_error_handling(method): """ Decorator (similar to `queries_records`) to handle exceptions and present in a way required for the search API. The decorator should be applied to search API endpoint - resources + resources. :param method: The method for the endpoint :raises: Any exception caught by the execution of `method` @@ -59,7 +58,7 @@ def assign_status_code(e, status_code): try: # If no status code exists (for non-API defined exceptions), assign a status # code - e.status_code + e.status_code # noqa: B018 except AttributeError: e.status_code = status_code @@ -79,7 +78,7 @@ def create_error_message(e): def get_search(entity_name, filters): """ Search for data on the given entity, using filters from the request to restrict the - query + query. :param entity_name: Name of the entity requested to query against :type entity_name: :class:`str` @@ -88,7 +87,6 @@ def get_search(entity_name, filters): :return: List of records (in JSON serialisable format) of the given entity for the query constructed from that and the request's filters """ - log.info("Searching for %s using request's filters", entity_name) log.debug("Entity Name: %s, Filters: %s", entity_name, filters) @@ -128,7 +126,7 @@ def get_search(entity_name, filters): @client_manager def get_with_pid(entity_name, pid, filters): """ - Get a particular record of data from the specified entity + Get a particular record of data from the specified entity. These will only be called with entity names of Dataset, Document and Instrument. Each of these entities have a PID attribute, so we can assume the identifier will be @@ -143,7 +141,6 @@ def get_with_pid(entity_name, pid, filters): :return: The (in JSON serialisable format) record of the specified PID :raises MissingRecordError: If no results can be found for the query """ - log.info("Getting %s from ID %s", entity_name, pid) log.debug("Entity Name: %s, Filters: %s", entity_name, filters) @@ -160,7 +157,7 @@ def get_with_pid(entity_name, pid, filters): def get_count(entity_name, filters): """ Get the number of results of a given entity, with filters provided in the request to - restrict the search + restrict the search. :param entity_name: Name of the entity requested to query against :type entity_name: :class:`str` @@ -168,7 +165,6 @@ def get_count(entity_name, filters): :type filters: List of specific implementation :class:`QueryFilter` :return: Dict containing the number of records returned from the query """ - log.info("Getting number of results for %s, using request's filters", entity_name) log.debug("Entity Name: %s, Filters: %s", entity_name, filters) @@ -190,7 +186,7 @@ def get_count(entity_name, filters): @client_manager def get_files(entity_name, pid, filters): """ - Using the PID of a dataset, find all of its associated files and return them + Using the PID of a dataset, find all of its associated files and return them. :param entity_name: Name of the entity requested to query against :type entity_name: :class:`str` @@ -200,7 +196,6 @@ def get_files(entity_name, pid, filters): :type filters: List of specific implementation :class:`QueryFilter` :return: List of file records for the dataset given by PID """ - log.info("Getting files of dataset (PID: %s), using request's filters", pid) log.debug( "Entity Name: %s, Filters: %s", @@ -215,7 +210,7 @@ def get_files(entity_name, pid, filters): @client_manager def get_files_count(entity_name, filters, pid): """ - Using the PID of a dataset, find the number of associated files + Using the PID of a dataset, find the number of associated files. :param entity_name: Name of the entity requested to query against :type entity_name: :class:`str` @@ -225,7 +220,6 @@ def get_files_count(entity_name, filters, pid): :type filters: List of specific implementation :class:`QueryFilter` :return: Dict containing the number of files for the dataset given by PID """ - log.info( "Getting number of files for dataset (PID: %s), using request's filters", pid, diff --git a/datagateway_api/search_api/models.py b/datagateway_api/search_api/models.py index 4b01f757..3bc91ec7 100644 --- a/datagateway_api/search_api/models.py +++ b/datagateway_api/search_api/models.py @@ -1,7 +1,7 @@ +import sys from abc import ABC, abstractmethod from datetime import datetime -import sys -from typing import Annotated, ClassVar, List, Literal, Optional, Union +from typing import Annotated, ClassVar, Literal, Optional from pydantic import ( BaseModel, @@ -79,7 +79,7 @@ def _get_icat_field_value(icat_field_name, icat_data): class PaNOSCAttribute(ABC, BaseModel): - _datetime_field_names: ClassVar[List[str]] = [ + _datetime_field_names: ClassVar[list[str]] = [ "creationDate", "startDate", "endDate", @@ -152,7 +152,7 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 required_related_fields_for_next_entity = [] for required_related_field in required_related_fields: - required_related_field = required_related_field.split(".") + required_related_field = required_related_field.split(".") # noqa: PLW2901 if len(required_related_field) > 1 and entity_field_alias in required_related_field: required_related_fields_for_next_entity.extend( required_related_field[1:], @@ -172,7 +172,7 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 entity_data[entity_field_alias] = field_value for required_related_field in required_related_fields: - required_related_field = required_related_field.split(".")[0] + required_related_field = required_related_field.split(".")[0] # noqa: PLW2901 if ( required_related_field in entity_fields @@ -201,35 +201,35 @@ def from_icat(cls, icat_data, required_related_fields): # noqa: B902, N805 class Affiliation(PaNOSCAttribute): - """Information about which facility a member is located at""" + """Information about which facility a member is located at.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["members"] - _text_operator_fields: ClassVar[List[str]] = [] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = ["members"] + _text_operator_fields: ClassVar[list[str]] = [] - name: Optional[str] = None - id_: Annotated[Optional[str], Field(alias="id")] - address: Optional[str] = None - city: Optional[str] = None - country: Optional[str] = None + name: str | None = None + id_: Annotated[str | None, Field(alias="id")] + address: str | None = None + city: str | None = None + country: str | None = None - members: Optional[List["Member"]] = [] + members: list["Member"] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Affiliation, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Dataset(PaNOSCAttribute): """ Information about an experimental run, including optional File, Sample, Instrument - and Technique + and Technique. """ - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [ + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [ "documents", "techniques", ] - _text_operator_fields: ClassVar[List[str]] = ["title"] + _text_operator_fields: ClassVar[list[str]] = ["title"] pid: SearchAPIPid title: str @@ -237,27 +237,25 @@ class Dataset(PaNOSCAttribute): # returned by it is public is_public: Literal[True] = Field(True, alias="isPublic") creation_date: SearchAPIDatetime = Field(alias="creationDate") - size: Optional[int] = None + size: int | None = None - documents: List["Document"] = [] - techniques: List["Technique"] = [] + documents: list["Document"] = [] + techniques: list["Technique"] = [] instrument: Optional["Instrument"] = None - files: Optional[List["File"]] = [] - parameters: Optional[List["Parameter"]] = [] - samples: Optional[List["Sample"]] = [] + files: list["File"] | None = [] + parameters: list["Parameter"] | None = [] + samples: list["Sample"] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Dataset, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Document(PaNOSCAttribute): - """ - Proposal which includes the dataset or published paper which references the dataset - """ + """Proposal which includes the dataset or published paper which references the dataset.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["datasets"] - _text_operator_fields: ClassVar[List[str]] = ["title", "summary"] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = ["datasets"] + _text_operator_fields: ClassVar[list[str]] = ["title", "summary"] pid: SearchAPIPid # Hardcoding this to True because anon user is used for querying so all data @@ -265,74 +263,74 @@ class Document(PaNOSCAttribute): is_public: Literal[True] = Field(True, alias="isPublic") type_: str = Field(alias="type") title: str - summary: Optional[str] = None - doi: Optional[str] = None - start_date: Optional[SearchAPIDatetime] = Field(None, alias="startDate") - end_date: Optional[SearchAPIDatetime] = Field(None, alias="endDate") - release_date: Optional[SearchAPIDatetime] = Field(None, alias="releaseDate") - license_: Optional[str] = Field(None, alias="license") - keywords: Optional[List[str]] = [] - - datasets: List[Dataset] = [] - members: Optional[List["Member"]] = [] - parameters: Optional[List["Parameter"]] = [] + summary: str | None = None + doi: str | None = None + start_date: SearchAPIDatetime | None = Field(None, alias="startDate") + end_date: SearchAPIDatetime | None = Field(None, alias="endDate") + release_date: SearchAPIDatetime | None = Field(None, alias="releaseDate") + license_: str | None = Field(None, alias="license") + keywords: list[str] | None = [] + + datasets: list[Dataset] = [] + members: list["Member"] | None = [] + parameters: list["Parameter"] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Document, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class File(PaNOSCAttribute): - """Name of file and optionally location""" + """Name of file and optionally location.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["dataset"] - _text_operator_fields: ClassVar[List[str]] = ["name"] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = ["dataset"] + _text_operator_fields: ClassVar[list[str]] = ["name"] id_: SearchAPIId name: str - path: Optional[str] = None - size: Optional[int] = None + path: str | None = None + size: int | None = None - dataset: Optional[Dataset] = None + dataset: Dataset | None = None @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(File, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Instrument(PaNOSCAttribute): - """Beam line where experiment took place""" + """Beam line where experiment took place.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] - _text_operator_fields: ClassVar[List[str]] = ["name", "facility"] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [] + _text_operator_fields: ClassVar[list[str]] = ["name", "facility"] pid: SearchAPIPid name: str facility: str - datasets: Optional[List[Dataset]] = [] + datasets: list[Dataset] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Instrument, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Member(PaNOSCAttribute): - """Proposal team member or paper co-author""" + """Proposal team member or paper co-author.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = ["document"] - _text_operator_fields: ClassVar[List[str]] = [] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = ["document"] + _text_operator_fields: ClassVar[list[str]] = [] id_: SearchAPIId - role: Optional[str] = Field(None, alias="role") + role: str | None = Field(None, alias="role") - document: Optional[Document] = None + document: Document | None = None person: Optional["Person"] = None - affiliation: Optional[Affiliation] = None + affiliation: Affiliation | None = None @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Member, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Parameter(PaNOSCAttribute): @@ -341,16 +339,16 @@ class Parameter(PaNOSCAttribute): Note: a parameter is either related to a dataset or a document, but not both. """ - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] - _text_operator_fields: ClassVar[List[str]] = [] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [] + _text_operator_fields: ClassVar[list[str]] = [] id_: SearchAPIId name: str - value: Union[float, int, str] - unit: Optional[str] = None + value: float | int | str + unit: str | None = None - dataset: Optional[Dataset] = None - document: Optional[Document] = None + dataset: Dataset | None = None + document: Document | None = None """ Validator commented as it was decided to be disabled for the time being. The Data @@ -376,60 +374,60 @@ def validate_dataset_and_document(cls, values): # noqa: B902, N805 @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Parameter, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Person(PaNOSCAttribute): - """Human who carried out experiment""" + """Human who carried out experiment.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] - _text_operator_fields: ClassVar[List[str]] = [] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [] + _text_operator_fields: ClassVar[list[str]] = [] id_: SearchAPIId full_name: str = Field(alias="fullName") - orcid: Optional[str] = None - researcher_id: Optional[str] = Field(None, alias="researcherId") - first_name: Optional[str] = Field(None, alias="firstName") - last_name: Optional[str] = Field(None, alias="lastName") + orcid: str | None = None + researcher_id: str | None = Field(None, alias="researcherId") + first_name: str | None = Field(None, alias="firstName") + last_name: str | None = Field(None, alias="lastName") - members: Optional[List[Member]] = [] + members: list[Member] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Person, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Sample(PaNOSCAttribute): - """Extract of material used in the experiment""" + """Extract of material used in the experiment.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] - _text_operator_fields: ClassVar[List[str]] = ["name", "description"] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [] + _text_operator_fields: ClassVar[list[str]] = ["name", "description"] name: str pid: SearchAPIPid - description: Optional[str] = None + description: str | None = None - datasets: Optional[List[Dataset]] = [] + datasets: list[Dataset] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Sample, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class Technique(PaNOSCAttribute): - """Common name of scientific method used""" + """Common name of scientific method used.""" - _related_fields_with_min_cardinality_one: ClassVar[List[str]] = [] - _text_operator_fields: ClassVar[List[str]] = ["name"] + _related_fields_with_min_cardinality_one: ClassVar[list[str]] = [] + _text_operator_fields: ClassVar[list[str]] = ["name"] pid: SearchAPIPid name: str - datasets: Optional[List[Dataset]] = [] + datasets: list[Dataset] | None = [] @classmethod def from_icat(cls, icat_data, required_related_fields): - return super(Technique, cls).from_icat(icat_data, required_related_fields) + return super().from_icat(icat_data, required_related_fields) class CountResponse(BaseModel): diff --git a/datagateway_api/search_api/nested_where_filters.py b/datagateway_api/search_api/nested_where_filters.py index 0c7396bd..af07eea2 100644 --- a/datagateway_api/search_api/nested_where_filters.py +++ b/datagateway_api/search_api/nested_where_filters.py @@ -13,7 +13,7 @@ def __init__(self, lhs, rhs, joining_operator, search_api_query=None): """ Class to represent nested conditions that use different boolean operators e.g. `(A OR B) AND (C OR D)`. This works by joining the two conditions with a boolean - operator + operator. :param lhs: Left hand side of the condition - either a string condition, WHERE filter or instance of this class @@ -27,7 +27,6 @@ def __init__(self, lhs, rhs, joining_operator, search_api_query=None): `rhs` (e.g. `AND` or `OR`) :type joining_operator: :class:`str` """ - # Ensure each side is in a list for consistency for string conversion if not isinstance(lhs, list): lhs = [lhs] @@ -49,7 +48,7 @@ def apply_filter(self, query): def set_search_api_query(query_filter, search_api_query): """ Using recursion, set the search API query to each of the WHERE filters contained - within the top-level `NestedWhereFilters` object + within the top-level `NestedWhereFilters` object. :param query_filter: Search API where filter or an instance of this class :type query_filter: :class:`NestedWhereFilters` or an object inherited from @@ -57,7 +56,6 @@ def set_search_api_query(query_filter, search_api_query): :param search_api_query: Search API query object :type search_api_query: :class:`SearchAPIQuery` """ - log.debug( "Setting SearchAPIQuery for NestedWhereFilters. Query filter: %s, Search API query: %s", repr(query_filter), @@ -91,10 +89,10 @@ def set_search_api_query(query_filter, search_api_query): def __str__(self): """ Join the condition on the left with the one on the right with the boolean - operator + operator. """ boolean_algebra_list = [self.lhs, self.rhs] - try: + try: # noqa: SIM105 boolean_algebra_list.remove([None]) except ValueError: # If neither side contains `None`, we should continue as normal diff --git a/datagateway_api/search_api/panosc_mappings.py b/datagateway_api/search_api/panosc_mappings.py index 5d7a51c2..7ea4cc7e 100644 --- a/datagateway_api/search_api/panosc_mappings.py +++ b/datagateway_api/search_api/panosc_mappings.py @@ -1,7 +1,7 @@ import json import logging -from pathlib import Path import sys +from pathlib import Path from datagateway_api.common.config import Config from datagateway_api.common.exceptions import FilterError, SearchAPIError @@ -11,8 +11,7 @@ class PaNOSCMappings: def __init__(self, path=None): - """Load contents of `search_api_mapping.json` into this class""" - + """Load contents of `search_api_mapping.json` into this class.""" if path is None: path = Path(__file__).parent.parent / "search_api_mapping.json" @@ -20,7 +19,7 @@ def __init__(self, path=None): with open(path, encoding="utf-8") as target: log.info("Loading PaNOSC to ICAT mappings from %s", path) self.mappings = json.load(target) - except IOError as e: + except OSError as e: # The API shouldn't exit if there's an exception (e.g. file not found) if # the user is only using DataGateway API and not the search API if Config.config.search_api: @@ -44,7 +43,6 @@ def get_icat_mapping(self, panosc_entity_name, field_name): mapping/translation from the PaNOSC data model :raises FilterError: If a valid mapping cannot be found """ - log.info( "Searching mapping file to find ICAT translation for %s", f"{panosc_entity_name}.{field_name}", @@ -77,7 +75,7 @@ def get_panosc_related_entity_name( ): """ For a given related field name (e.g. "files"), get the entity name version of - this (e.g. "File") + this (e.g. "File"). :param panosc_entity_name: Entity name used as an entrypoint into the mapping :type panosc_entity_name: :class:`str` @@ -87,7 +85,6 @@ def get_panosc_related_entity_name( :return: Entity name for the given related field name :raises SearchAPIError: If a suitable mapping cannot be found """ - panosc_related_entity_name = "" try: panosc_related_entity_name = list( @@ -122,9 +119,7 @@ def get_panosc_non_related_field_names(self, panosc_entity_name): for mapping_key, mapping_value in entity_mappings.items(): # The mappings for the non-related fields are of type `str` and sometimes # `list' whereas for the related fields, they are of type `dict`. - if mapping_key != "base_icat_entity" and ( - isinstance(mapping_value, str) or isinstance(mapping_value, list) - ): + if mapping_key != "base_icat_entity" and (isinstance(mapping_value, (str, list))): non_related_field_names.append(mapping_key) return non_related_field_names diff --git a/datagateway_api/search_api/query.py b/datagateway_api/search_api/query.py index bf88af68..8267cc87 100644 --- a/datagateway_api/search_api/query.py +++ b/datagateway_api/search_api/query.py @@ -27,7 +27,7 @@ class SearchAPIICATQuery(ICATQuery): """ Class which has identical functionality to `ICATQuery` but uses a different `__init__` to call `ConditionSettingQuery` instead of the base `Query`. That class - contains features required for the search API + contains features required for the search API. """ def __init__( diff --git a/datagateway_api/search_api/query_filter_factory.py b/datagateway_api/search_api/query_filter_factory.py index ae442714..e6729095 100644 --- a/datagateway_api/search_api/query_filter_factory.py +++ b/datagateway_api/search_api/query_filter_factory.py @@ -1,6 +1,6 @@ import logging - +import datagateway_api.search_api.models as search_api_models from datagateway_api.common.base_query_filter_factory import QueryFilterFactory from datagateway_api.common.config import Config from datagateway_api.common.exceptions import FilterError, SearchAPIError @@ -11,7 +11,6 @@ SearchAPISkipFilter, SearchAPIWhereFilter, ) -import datagateway_api.search_api.models as search_api_models from datagateway_api.search_api.nested_where_filters import NestedWhereFilters from datagateway_api.search_api.panosc_mappings import mappings from datagateway_api.search_api.query import SearchAPIQuery @@ -23,7 +22,7 @@ class SearchAPIQueryFilterFactory(QueryFilterFactory): @staticmethod def get_query_filter(request_filter, entity_name=None, related_entity_name=None): """ - Given a filter, return a list of matching query filter objects + Given a filter, return a list of matching query filter objects. :param request_filter: The filter from which to create a list of query filter objects @@ -103,7 +102,7 @@ def get_query_filter(request_filter, entity_name=None, related_entity_name=None) def get_where_filter(where_filter_input, entity_name, related_entity_name=None): """ Given a where filter input, return a list of `NestedWhereFilters` and/ or - `SearchAPIWhereFilter` objects + `SearchAPIWhereFilter` objects. `NestedWhereFilters` objects are created when there is an AND or OR inside the where filter input, otherwise `SearchAPIWhereFilter` objects are created. If @@ -126,7 +125,6 @@ def get_where_filter(where_filter_input, entity_name, related_entity_name=None): created :raises SearchAPIError: If there are no text operator fields on the entity """ - where_filters = [] if list(where_filter_input.keys())[0] == "and" or list(where_filter_input.keys())[0] == "or": log.debug("and/or operators found: %s", list(where_filter_input.keys())[0]) @@ -220,7 +218,7 @@ def get_include_filter(include_filter_input, entity_name, related_entity_name=No """ Given an include filter input, return a list of `SearchAPIIncludeFilter` and any `NestedWhereFilters` and/ or `SearchAPIWhereFilter` objects if there is a scope - filter inside the filter input + filter inside the filter input. Currently, we do not support limit and skip filters inside scope filters that are part of include filters. @@ -240,7 +238,6 @@ def get_include_filter(include_filter_input, entity_name, related_entity_name=No or `SearchAPIWhereFilter` objects created :raises FilterError: If scope filter has a limit or skip filter """ - query_filters = [] included_entities = [] @@ -312,7 +309,7 @@ def get_include_filter(include_filter_input, entity_name, related_entity_name=No def get_condition_values(conditions_dict): """ Given a simplified where filter input, return a field name, value and operation - as a tuple + as a tuple. :param conditions_dict: The filter from which to return a field name, value and operation @@ -347,7 +344,7 @@ def prefix_where_filter_field_with_entity_name(where_filters, entity_name): """ Given a `NestedWhereFilters` or `SearchAPIWhereFilter` object, or a list of these objects, prefix the field attribute of the `SearchAPIWhereFilter` object - with the provided entity name + with the provided entity name. When dealing with `NestedWhereFilters`, this function is called recursively in order to drill down and get hold of the `SearchAPIWhereFilter` objects so that diff --git a/datagateway_api/search_api/routers/entity.py b/datagateway_api/search_api/routers/entity.py index a26fd891..21bdb726 100644 --- a/datagateway_api/search_api/routers/entity.py +++ b/datagateway_api/search_api/routers/entity.py @@ -1,5 +1,5 @@ import logging -from typing import Annotated, Any, List +from typing import Annotated, Any from fastapi import APIRouter, Path, Query, Request from pydantic import BaseModel @@ -135,7 +135,7 @@ def get_search_endpoint( "", summary=f"Get {entity_name}s", description=f"Retrieves a list of {entity_name} objects", - response_model=List[model], + response_model=list[model], responses={ 200: {"description": (f"Success - returns {entity_name}s that satisfy the filter")}, 400: {"description": "Bad request - Something was wrong with the request"}, @@ -145,7 +145,7 @@ def get_search_endpoint( @search_api_error_handling def get( request: Request, - filter_: str = FilterQuery, # pylint:disable=unused-argument + filter_: str = FilterQuery, # noqa: ARG001 ): filters = get_filters_from_query_string( request, @@ -191,7 +191,7 @@ def get_single_endpoint( def get( request: Request, pid: Annotated[str, Path(description="The pid of the entity to retrieve")], - filter_: str = FilterQuery, # pylint:disable=unused-argument + filter_: str = FilterQuery, # noqa: ARG001 ): filters = get_filters_from_query_string( request, @@ -230,7 +230,7 @@ def get_number_count_endpoint( @search_api_error_handling def get( request: Request, - where: Any = WhereQuery, # pylint:disable=unused-argument + where: Any = WhereQuery, # noqa: ARG001 ): filters = get_filters_from_query_string( request, @@ -254,7 +254,7 @@ def get_files_endpoint(router: APIRouter, entity_name: str) -> None: "/{pid}/files", summary=f"Get {entity_name}s for the given Dataset", description=(f"Retrieves a list of {entity_name} objects for a given Dataset object"), - response_model=List[search_api_models.File], + response_model=list[search_api_models.File], responses={ 200: {"description": (f"Success - returns {entity_name}s for the given Dataset")}, 400: {"description": "Bad request - Something was wrong with the request"}, @@ -265,7 +265,7 @@ def get_files_endpoint(router: APIRouter, entity_name: str) -> None: def get( request: Request, pid: Annotated[str, Path(description="The pid of the entity to retrieve")], - filter_: str = FilterQuery, # pylint:disable=unused-argument + filter_: str = FilterQuery, # noqa: ARG001 ): filters = get_filters_from_query_string( request, @@ -306,7 +306,7 @@ def get_number_count_files_endpoint( def get( request: Request, pid: Annotated[str, Path(description="The pid of the entity to retrieve")], - where: Any = WhereQuery, # pylint:disable=unused-argument + where: Any = WhereQuery, # noqa: ARG001 ): filters = get_filters_from_query_string( request, diff --git a/datagateway_api/search_api/search_scoring.py b/datagateway_api/search_api/search_scoring.py index 585132e3..89af1eb3 100644 --- a/datagateway_api/search_api/search_scoring.py +++ b/datagateway_api/search_api/search_scoring.py @@ -15,7 +15,7 @@ def get_score(query): :type query: :class:`str` :return: Returns the scores :raises ScoringAPIError: If an error occurs while interacting with the Search - Scoring API + Scoring API. """ try: data = { @@ -48,7 +48,7 @@ def add_scores_to_results(results, scores): :type results: :class:`list` :param scores: List of items retrieved from the scoring application :type scores: :class:`list` - :return: Returns the results with scores + :return: Returns the results with scores. """ for result in results: result["score"] = next( diff --git a/datagateway_api/search_api/session_handler.py b/datagateway_api/search_api/session_handler.py index 2004281c..64f86cf5 100644 --- a/datagateway_api/search_api/session_handler.py +++ b/datagateway_api/search_api/session_handler.py @@ -1,5 +1,5 @@ -from functools import wraps import logging +from functools import wraps from icat.exception import ICATSessionError @@ -13,7 +13,7 @@ class SessionHandler: """ Class to store Python ICAT client to be used within the search API. As the API requires no authentication, the same client object can be used which logs in as the - anon user + anon user. """ client = ICATClient(client_use="search_api") @@ -23,7 +23,7 @@ def client_manager(method): """ Decorator to manage the client object at the beginning of each request. This decorator checks if the client has a valid session, and if not, logs in as the anon - user + user. :param method: The function used to process an incoming request """ diff --git a/pyproject.toml b/pyproject.toml index 0cf11618..3e49fb09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,18 +38,7 @@ build-backend = "uv_build" [dependency-groups] code-analysis = [ { include-group = "test" }, - "black>=24.10.0,<25", - "flake8>=7.1.1,<8", - "flake8-bandit>=4.1.1,<5", - "flake8-black>=0.3.6,<0.4", - "flake8-broken-line>=1.0.0,<2", - "flake8-bugbear>=24.8.19,<25", - "flake8-builtins>=2.5.0,<3", - "flake8-commas>=4.0.0,<5", - "flake8-comprehensions>=3.15.0,<4", - "flake8-import-order>=0.18.2,<0.19", - "flake8-logging-format>=1.0.0,<2", - "pep8-naming>=0.14.1,<0.15", + "ruff>=0.15.16", ] test = [ @@ -68,6 +57,7 @@ dev = [ { include-group = "code-analysis" }, { include-group = "test" }, { include-group = "scripts" }, + ] @@ -75,7 +65,3 @@ dev = [ omit = [ "test/*", ] - - -[tool.black] -line-length = 120 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..ac8386a5 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,40 @@ +# We prefer a longer line length than the default +line-length = 120 + +[lint] +select = [ + # Ruff default + "E", + "F", + # General recommended plugins + "B", # flake8-bugbear - Bugs and bad patterns + "I", # Consistent import order + "UP", # pyupgrade - Ensures modern python syntax + "PL", # pylint - Used on other repos + # Additional useful flake8 plugins + "SIM", # flake8-simplify - Suggests simplified code + "C4", # flake8-comprehensions - Suggests simplified list, set, dict comprehensions + "ARG", # flake8-unused-arguments + "TID", # flake8-tidy-imports, + "N", # pep8-naming + "A", # flake8-builtins + "G", # flake8-logging-format + "ISC", # line breaks +] + + +ignore = [ + "PLR0912", # too-many-branches - allows complex functions without forcing excessive refactoring + "PLR0913", # too-many-arguments - allows functions/methods with many parameters (common in APIs/tests) +] + + +[lint.per-file-ignores] +"test/**/*.py" = [ + "ARG002", # unused function arguments (pytest fixtures used for setup) +] + + +[lint.isort] +known-first-party = ["datagateway_api"] +known-local-folder = ["test"] \ No newline at end of file diff --git a/test/conftest.py b/test/conftest.py index a111913d..6fa0aaf7 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,8 @@ import json from unittest.mock import mock_open, patch -from icat.query import Query import pytest +from icat.query import Query from datagateway_api.common.config import APIConfig diff --git a/test/integration/conftest.py b/test/integration/conftest.py index 09bf43ff..ead5e562 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -1,12 +1,11 @@ -from datetime import datetime, timedelta import json +from datetime import datetime, timedelta from unittest.mock import mock_open, patch +import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from icat.client import Client -import pytest - from datagateway_api.common.config import APIConfig, Config from datagateway_api.datagateway_api.icat.models import Session @@ -38,10 +37,7 @@ def fixture_test_client() -> TestClient: @pytest.fixture(name="local_auth_client") def fixture_auth_test_client() -> TestClient: - """ - Isolated TestClient for auth tests. - """ - + """Isolated TestClient for auth tests.""" auth_app = FastAPI() register_common_handlers(auth_app) return TestClient(auth_app) diff --git a/test/integration/datagateway_api/icat/conftest.py b/test/integration/datagateway_api/icat/conftest.py index 2211b46a..12e572ae 100644 --- a/test/integration/datagateway_api/icat/conftest.py +++ b/test/integration/datagateway_api/icat/conftest.py @@ -1,10 +1,10 @@ -from datetime import datetime import json import uuid +from datetime import datetime +import pytest from dateutil.tz import tzlocal from icat.exception import ICATNoObjectError -import pytest from test.integration.datagateway_api.icat.endpoints.test_create_icat import ( TestICATCreateData, @@ -112,14 +112,13 @@ def remove_test_created_investigation_data( ): """ This is used to delete the data created inside `test_valid` test functions in - TestICATCreateData + TestICATCreateData. This is done by fetching the data which has been created in those functions (by using the investigation name prefix, as defined in the test class), extracting the IDs from the results, and iterating over those to perform DELETE by ID requests """ - yield where = {"name": {"like": TestICATCreateData.investigation_name_prefix}} diff --git a/test/integration/datagateway_api/icat/endpoints/test_count_with_filters_icat.py b/test/integration/datagateway_api/icat/endpoints/test_count_with_filters_icat.py index 4de96c50..dd155fcc 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_count_with_filters_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_count_with_filters_icat.py @@ -38,8 +38,8 @@ def test_valid_no_results_count_with_filters( valid_icat_credentials_header, ): test_response = test_client.get( - '/datagateway-api/investigations/count?where={"title": {"like": "This filter should cause 0 results to be found ' - 'for testing purposes..."}}', + '/datagateway-api/investigations/count?where={"title": {"like": "This filter should cause 0 results to be ' + 'found for testing purposes..."}}', headers=valid_icat_credentials_header, ) diff --git a/test/integration/datagateway_api/icat/endpoints/test_create_icat.py b/test/integration/datagateway_api/icat/endpoints/test_create_icat.py index b4d53aa4..98741e56 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_create_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_create_icat.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status from test.integration.datagateway_api.icat.test_query import ( prepare_icat_data_for_assertion, @@ -75,8 +76,7 @@ def test_valid_boundary_create_data( test_client, valid_icat_credentials_header, ): - """Create a single investigation, as opposed to multiple""" - + """Create a single investigation, as opposed to multiple.""" create_investigation_json = [ { "name": f"{self.investigation_name_prefix} 0", @@ -115,8 +115,7 @@ def test_invalid_create_data( test_client, valid_icat_credentials_header, ): - """An investigation requires a minimum of: name, visitId, facility, type""" - + """An investigation requires a minimum of: name, visitId, facility, type.""" invalid_request_body = [ { "title": "Test Title for DataGateway API Python ICAT testing", @@ -129,7 +128,7 @@ def test_invalid_create_data( json=invalid_request_body, ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST def test_invalid_existing_data_create( self, @@ -137,8 +136,7 @@ def test_invalid_existing_data_create( valid_icat_credentials_header, single_investigation_test_data, ): - """This test targets raising ICATObjectExistsError, causing a 400""" - + """This test targets raising ICATObjectExistsError, causing a 400.""" # entity.as_dict() removes details about facility and type, hence they're # hardcoded here instead of using sinle_investigation_test_data existing_object_json = [ @@ -157,7 +155,7 @@ def test_invalid_existing_data_create( json=existing_object_json, ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST def test_valid_rollback_behaviour( self, @@ -191,11 +189,11 @@ def test_valid_rollback_behaviour( get_response = test_client.get( "/datagateway-api/investigations?where=" # noqa: B907 '{"title": {"eq": "' - f'{request_body[0]["title"]}' + f"{request_body[0]['title']}" '"}}', headers=valid_icat_credentials_header, ) get_response_json = prepare_icat_data_for_assertion(get_response.json()) - assert create_response.status_code == 400 + assert create_response.status_code == status.HTTP_400_BAD_REQUEST assert get_response_json == [] diff --git a/test/integration/datagateway_api/icat/endpoints/test_delete_by_id_icat.py b/test/integration/datagateway_api/icat/endpoints/test_delete_by_id_icat.py index 2a10e966..4815bb7b 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_delete_by_id_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_delete_by_id_icat.py @@ -1,3 +1,6 @@ +from fastapi import status + + class TestDeleteByID: def test_valid_delete_with_id( self, @@ -6,19 +9,18 @@ def test_valid_delete_with_id( single_investigation_test_data, ): test_response = test_client.delete( - f'/datagateway-api/investigations/{single_investigation_test_data[0]["id"]}', + f"/datagateway-api/investigations/{single_investigation_test_data[0]['id']}", headers=valid_icat_credentials_header, ) - assert test_response.status_code == 204 + assert test_response.status_code == status.HTTP_204_NO_CONTENT def test_invalid_delete_with_id( self, test_client, valid_icat_credentials_header, ): - """Request with a non-existent ID""" - + """Request with a non-existent ID.""" final_investigation_result = test_client.get( '/datagateway-api/investigations/findone?order="id DESC"', headers=valid_icat_credentials_header, @@ -32,4 +34,4 @@ def test_invalid_delete_with_id( headers=valid_icat_credentials_header, ) - assert test_response.status_code == 404 + assert test_response.status_code == status.HTTP_404_NOT_FOUND diff --git a/test/integration/datagateway_api/icat/endpoints/test_findone_icat.py b/test/integration/datagateway_api/icat/endpoints/test_findone_icat.py index b99178b3..f1bdaaac 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_findone_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_findone_icat.py @@ -1,3 +1,5 @@ +from fastapi import status + from test.integration.datagateway_api.icat.test_query import ( prepare_icat_data_for_assertion, ) @@ -11,7 +13,8 @@ def test_valid_findone_with_filters( single_investigation_test_data, ): test_response = test_client.get( - '/datagateway-api/investigations/findone?where={"title": {"like": "Test data for Python ICAT on DataGateway API"}}', + '/datagateway-api/investigations/findone?where={"title": {"like": "Test data for Python ICAT on ' + 'DataGateway API"}}', headers=valid_icat_credentials_header, ) response_json = prepare_icat_data_for_assertion([test_response.json()]) @@ -29,4 +32,4 @@ def test_valid_no_results_findone_with_filters( headers=valid_icat_credentials_header, ) - assert test_response.status_code == 404 + assert test_response.status_code == status.HTTP_404_NOT_FOUND diff --git a/test/integration/datagateway_api/icat/endpoints/test_get_by_id_icat.py b/test/integration/datagateway_api/icat/endpoints/test_get_by_id_icat.py index f5b93b66..0e81471f 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_get_by_id_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_get_by_id_icat.py @@ -1,3 +1,5 @@ +from fastapi import status + from test.integration.datagateway_api.icat.test_query import ( prepare_icat_data_for_assertion, ) @@ -32,8 +34,7 @@ def test_invalid_get_with_id( test_client, valid_icat_credentials_header, ): - """Request with a non-existent ID""" - + """Request with a non-existent ID.""" final_investigation_result = test_client.get( '/datagateway-api/investigations/findone?order="id DESC"', headers=valid_icat_credentials_header, @@ -46,4 +47,4 @@ def test_invalid_get_with_id( headers=valid_icat_credentials_header, ) - assert test_response.status_code == 404 + assert test_response.status_code == status.HTTP_404_NOT_FOUND diff --git a/test/integration/datagateway_api/icat/endpoints/test_get_with_filters_icat.py b/test/integration/datagateway_api/icat/endpoints/test_get_with_filters_icat.py index 54ec3646..6de20b62 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_get_with_filters_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_get_with_filters_icat.py @@ -26,7 +26,8 @@ def test_valid_no_results_get_with_filters( valid_icat_credentials_header, ): test_response = test_client.get( - '/datagateway-api/investigations?where={"title": {"eq": "This filter should cause a 404 for testing purposes..."}}', + '/datagateway-api/investigations?where={"title": {"eq": "This filter should cause a 404 for testing ' + 'purposes..."}}', headers=valid_icat_credentials_header, ) diff --git a/test/integration/datagateway_api/icat/endpoints/test_ping_icat.py b/test/integration/datagateway_api/icat/endpoints/test_ping_icat.py index 559781c4..7f7c4383 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_ping_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_ping_icat.py @@ -1,7 +1,7 @@ from unittest.mock import patch -from icat.exception import ICATError import pytest +from icat.exception import ICATError from datagateway_api.common.constants import Constants from datagateway_api.common.exceptions import PythonICATError diff --git a/test/integration/datagateway_api/icat/endpoints/test_update_by_id_icat.py b/test/integration/datagateway_api/icat/endpoints/test_update_by_id_icat.py index e47376a0..d1b9b26b 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_update_by_id_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_update_by_id_icat.py @@ -1,3 +1,5 @@ +from fastapi import status + from test.integration.datagateway_api.icat.test_query import ( prepare_icat_data_for_assertion, ) @@ -68,8 +70,7 @@ def test_invalid_update_with_id( valid_icat_credentials_header, single_investigation_test_data, ): - """This test will attempt to put `icatdb` into an invalid state""" - + """This test will attempt to put `icatdb` into an invalid state.""" # DOI cannot be over 255 characters, which this string is invalid_update_json = { "doi": "_" * 256, @@ -81,4 +82,4 @@ def test_invalid_update_with_id( json=invalid_update_json, ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/test/integration/datagateway_api/icat/endpoints/test_update_multiple_icat.py b/test/integration/datagateway_api/icat/endpoints/test_update_multiple_icat.py index 280e66c3..0a3ef2b5 100644 --- a/test/integration/datagateway_api/icat/endpoints/test_update_multiple_icat.py +++ b/test/integration/datagateway_api/icat/endpoints/test_update_multiple_icat.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status from test.integration.datagateway_api.icat.test_query import ( prepare_icat_data_for_assertion, @@ -43,8 +44,7 @@ def test_valid_boundary_update_data( valid_icat_credentials_header, single_investigation_test_data, ): - """Request body is a dictionary, not a list of dictionaries""" - + """Request body is a dictionary, not a list of dictionaries.""" expected_doi = "Test Data Identifier" expected_summary = "Test Summary" @@ -73,8 +73,7 @@ def test_invalid_missing_update_data( valid_icat_credentials_header, single_investigation_test_data, ): - """There should be an ID in the request body to know which entity to update""" - + """There should be an ID in the request body to know which entity to update.""" update_data_json = [ { "doi": "Test Data Identifier", @@ -88,7 +87,7 @@ def test_invalid_missing_update_data( json=update_data_json, ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.parametrize( "update_key, update_value", @@ -135,7 +134,6 @@ def test_valid_rollback_behaviour( will throw an ICAT related exception. At this point, the rollback behaviour should execute, restoring the state of the first record (i.e. un-updating it) """ - request_body = [ { "id": multiple_investigation_test_data[0]["id"], @@ -159,6 +157,6 @@ def test_valid_rollback_behaviour( ) get_response_json = prepare_icat_data_for_assertion([get_response.json()]) - assert update_response.status_code == 500 + assert update_response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR # RHS encased in a list as prepare_icat_data_for_assertion() always returns list assert get_response_json == [multiple_investigation_test_data[0]] diff --git a/test/integration/datagateway_api/icat/filters/test_limit_filter.py b/test/integration/datagateway_api/icat/filters/test_limit_filter.py index ce01068b..1515db4e 100644 --- a/test/integration/datagateway_api/icat/filters/test_limit_filter.py +++ b/test/integration/datagateway_api/icat/filters/test_limit_filter.py @@ -5,9 +5,9 @@ from datagateway_api.common.exceptions import FilterError from datagateway_api.common.filter_order_handler import FilterOrderHandler from datagateway_api.datagateway_api.icat.filters import ( - icat_set_limit, PythonICATLimitFilter, PythonICATSkipFilter, + icat_set_limit, ) @@ -46,7 +46,7 @@ def test_invalid_limit_value(self, icat_query, limit_value): def test_limit_and_skip_merge_correctly(self, icat_query, skip_value, limit_value): """ Skip and limit values are set together in Python ICAT, limit value should match - max entities allowed in one transaction in ICAT as defined in ICAT properties + max entities allowed in one transaction in ICAT as defined in ICAT properties. """ skip_filter = PythonICATSkipFilter(skip_value) limit_filter = PythonICATLimitFilter(limit_value) @@ -61,11 +61,13 @@ def test_limit_and_skip_merge_correctly(self, icat_query, skip_value, limit_valu def test_invalid_icat_set_limit(self, icat_query): """ The validity of this function is tested when applying a limit filter, an explict - invalid test case is required to cover when invalid arguments are given + invalid test case is required to cover when invalid arguments are given. """ - with patch( - "icat.query.Query.setLimit", - side_effect=TypeError("Mocked Exception"), + with ( + patch( + "icat.query.Query.setLimit", + side_effect=TypeError("Mocked Exception"), + ), + pytest.raises(FilterError), ): - with pytest.raises(FilterError): - icat_set_limit(icat_query, 50, 50) + icat_set_limit(icat_query, 50, 50) diff --git a/test/integration/datagateway_api/icat/filters/test_order_filter.py b/test/integration/datagateway_api/icat/filters/test_order_filter.py index 2ef05d3f..2891e11c 100644 --- a/test/integration/datagateway_api/icat/filters/test_order_filter.py +++ b/test/integration/datagateway_api/icat/filters/test_order_filter.py @@ -1,5 +1,6 @@ +from collections import OrderedDict + import pytest -from typing_extensions import OrderedDict from datagateway_api.common.exceptions import FilterError from datagateway_api.common.filter_order_handler import FilterOrderHandler @@ -8,7 +9,7 @@ class TestICATOrderFilter: def test_direction_is_uppercase(self, icat_query): - """Direction must be uppercase for Python ICAT to see the input as valid""" + """Direction must be uppercase for Python ICAT to see the input as valid.""" test_filter = PythonICATOrderFilter("id", "asc") filter_handler = FilterOrderHandler() diff --git a/test/integration/datagateway_api/icat/filters/test_where_filter.py b/test/integration/datagateway_api/icat/filters/test_where_filter.py index 904a484f..79505031 100644 --- a/test/integration/datagateway_api/icat/filters/test_where_filter.py +++ b/test/integration/datagateway_api/icat/filters/test_where_filter.py @@ -95,7 +95,7 @@ def test_invalid_operation(self, icat_query): test_filter.apply_filter(icat_query) def test_valid_internal_icat_value(self, icat_query): - """Check that values that point to other values in the schema are applied""" + """Check that values that point to other values in the schema are applied.""" test_filter = PythonICATWhereFilter("startDate", "o.endDate", "lt") test_filter.apply_filter(icat_query) diff --git a/test/integration/datagateway_api/icat/test_helpers.py b/test/integration/datagateway_api/icat/test_helpers.py index 5006b243..3716503a 100644 --- a/test/integration/datagateway_api/icat/test_helpers.py +++ b/test/integration/datagateway_api/icat/test_helpers.py @@ -1,14 +1,14 @@ from unittest.mock import patch -from icat.exception import ICATInternalError import pytest +from icat.exception import ICATInternalError from datagateway_api.common.exceptions import PythonICATError from datagateway_api.datagateway_api.icat.helpers import push_data_updates_to_icat class TestICATHelpers: - """Testing the helper functions which aren't covered in the endpoint tests""" + """Testing the helper functions which aren't covered in the endpoint tests.""" def test_invalid_update_pushes(self, icat_client): with patch( diff --git a/test/integration/datagateway_api/icat/test_icat_client.py b/test/integration/datagateway_api/icat/test_icat_client.py index fe2907f6..342faadf 100644 --- a/test/integration/datagateway_api/icat/test_icat_client.py +++ b/test/integration/datagateway_api/icat/test_icat_client.py @@ -1,7 +1,7 @@ from unittest.mock import patch -from icat.client import Client import pytest +from icat.client import Client from datagateway_api.datagateway_api.icat.icat_client_pool import ICATClient @@ -44,17 +44,19 @@ def __init__(url, checkCert=True): # noqa Client.url = f"{url}/ICATService/ICAT?wsdl" Client.checkCert = checkCert - with patch( - "datagateway_api.common.config.Config.config", - test_config, - ): - with patch( + with ( + patch( + "datagateway_api.common.config.Config.config", + test_config, + ), + patch( "icat.client.Client.__init__", side_effect=MockClient.__init__, - ): - test_icat_client = ICATClient(client_use) - assert test_icat_client.url == expected_url - assert test_icat_client.checkCert == expected_check_cert + ), + ): + test_icat_client = ICATClient(client_use) + assert test_icat_client.url == expected_url + assert test_icat_client.checkCert == expected_check_cert def test_clean_up(self): test_icat_client = ICATClient() diff --git a/test/integration/datagateway_api/icat/test_lru_cache.py b/test/integration/datagateway_api/icat/test_lru_cache.py index 8186a07c..262427c7 100644 --- a/test/integration/datagateway_api/icat/test_lru_cache.py +++ b/test/integration/datagateway_api/icat/test_lru_cache.py @@ -24,7 +24,7 @@ def test_valid_popitem(self): test_cache.popitem = MagicMock(side_effect=test_cache.popitem) @cached(cache=test_cache) - def get_cached_client(cache_number, client_pool): + def get_cached_client(_cache_number, _client_pool): return test_client for cache_number in range(Config.config.datagateway_api.client_cache_size + 1): diff --git a/test/integration/datagateway_api/icat/test_query.py b/test/integration/datagateway_api/icat/test_query.py index 8e425f0a..453d6114 100644 --- a/test/integration/datagateway_api/icat/test_query.py +++ b/test/integration/datagateway_api/icat/test_query.py @@ -1,7 +1,7 @@ from datetime import datetime -from icat.entity import Entity import pytest +from icat.entity import Entity from datagateway_api.common.date_handler import DateHandler from datagateway_api.common.exceptions import PythonICATError @@ -19,45 +19,45 @@ def prepare_icat_data_for_assertion( ): """ Remove meta attributes from ICAT data. Meta attributes contain data about data - creation/modification, and should be removed to ensure correct assertion values + creation/modification, and should be removed to ensure correct assertion values. :param data: ICAT data containing meta attributes such as modTime :type data: :class:`list` or :class:`icat.entity.EntityList` """ + assertable_data = [] meta_attributes = Entity.MetaAttr for entity in data: # Convert to dictionary if an ICAT entity object - if isinstance(entity, Entity): - entity = entity.as_dict() + entity_dict = entity.as_dict() if isinstance(entity, Entity) else entity for attr in meta_attributes: - entity.pop(attr) + entity_dict.pop(attr) - for key in entity: - if isinstance(entity[key], dict): + for key in entity_dict: + if isinstance(entity_dict[key], dict): for attr in meta_attributes: - entity[key].pop(attr) + entity_dict[key].pop(attr) - for attr in entity.keys(): - if isinstance(entity[attr], datetime): - entity[attr] = DateHandler.datetime_object_to_str(entity[attr]) + for attr in entity_dict: + if isinstance(entity_dict[attr], datetime): + entity_dict[attr] = DateHandler.datetime_object_to_str(entity_dict[attr]) # meta_attributes is immutable if remove_id: - entity.pop("id") + entity_dict.pop("id") if remove_visit_id: - entity.pop("visitId") + entity_dict.pop("visitId") + + assertable_data.append(entity_dict) - assertable_data.append(entity) return assertable_data class TestICATQuery: @pytest.mark.parametrize( - "input_conditions, input_aggregate, input_includes, expected_conditions," - " expected_aggregate, expected_includes", + "input_conditions, input_aggregate, input_includes, expected_conditions, expected_aggregate, expected_includes", [ pytest.param( {"fullName": "like Bob"}, @@ -114,7 +114,7 @@ def test_valid_query_creation( def test_valid_manual_count_flag_init(self, icat_client): """ Flag required for distinct filters used on count endpoints should be initialised - in `__init__()` of ICATQuery` + in `__init__()` of ICATQuery`. """ test_query = ICATQuery(icat_client, "User") @@ -285,7 +285,7 @@ def test_valid_query_execution( return_json_formattable=return_json_format_flag, ) - if test_query.query.aggregate != "COUNT" and test_query.query.aggregate != "DISTINCT": + if test_query.query.aggregate not in {"COUNT", "DISTINCT"}: query_data = prepare_icat_data_for_assertion( query_data, remove_id=True, diff --git a/test/integration/datagateway_api/icat/test_reader_performance.py b/test/integration/datagateway_api/icat/test_reader_performance.py index 122c58b9..1429fde1 100644 --- a/test/integration/datagateway_api/icat/test_reader_performance.py +++ b/test/integration/datagateway_api/icat/test_reader_performance.py @@ -1,9 +1,9 @@ -from datetime import datetime import time -from typing import Generator +from collections.abc import Generator +from datetime import datetime -from icat.client import Client import pytest +from icat.client import Client from datagateway_api.common.config import APIConfig, Config from datagateway_api.common.exceptions import MissingRecordError, PythonICATError diff --git a/test/integration/datagateway_api/icat/test_session_handling.py b/test/integration/datagateway_api/icat/test_session_handling.py index bc47f28d..4c4ccf81 100644 --- a/test/integration/datagateway_api/icat/test_session_handling.py +++ b/test/integration/datagateway_api/icat/test_session_handling.py @@ -1,9 +1,10 @@ from datetime import datetime from unittest.mock import patch +import pytest from dateutil.tz import tzlocal +from fastapi import status from icat.client import Client -import pytest from datagateway_api.common.config import Config from datagateway_api.common.date_handler import DateHandler @@ -32,8 +33,11 @@ def test_get_valid_session_details( time_diff = abs(session_expiry_datetime - current_datetime) time_diff_minutes = time_diff.seconds / 60 + max_expected_minutes = 120 + min_expected_minutes = 118 + # Allows a bit of leeway for slow test execution - assert time_diff_minutes < 120 and time_diff_minutes >= 118 + assert min_expected_minutes <= time_diff_minutes < max_expected_minutes # Check username is correct test_mechanism = Config.config.test_mechanism @@ -53,7 +57,7 @@ def test_get_invalid_session_details( headers=bad_credentials_header, ) - assert session_details.status_code == 403 + assert session_details.status_code == status.HTTP_403_FORBIDDEN def test_refresh_session(self, valid_icat_credentials_header, test_client): pre_refresh_session_details = test_client.get( @@ -71,7 +75,7 @@ def test_refresh_session(self, valid_icat_credentials_header, test_client): headers=valid_icat_credentials_header, ) - assert refresh_session.status_code == 200 + assert refresh_session.status_code == status.HTTP_200_OK assert ( pre_refresh_session_details.json()["expireDateTime"] @@ -122,7 +126,7 @@ def test_valid_login( test_query = icat_client.search(icat_query) - assert test_query == [1] and login_response.status_code == 201 + assert test_query == [1] and login_response.status_code == status.HTTP_201_CREATED @pytest.mark.parametrize( "request_body, expected_response_code", @@ -133,7 +137,7 @@ def test_valid_login( "password": "InvalidPassword", "mechanism": Config.config.test_mechanism, }, - 403, + status.HTTP_403_FORBIDDEN, id="Invalid credentials", ), pytest.param({}, 422, id="Missing credentials"), @@ -155,12 +159,11 @@ def test_invalid_login( def test_expired_session(self): test_python_icat = PythonICAT() client_pool = create_client_pool() - with patch("icat.client.Client.getRemainingMinutes", return_value=-1): - with pytest.raises(AuthenticationError): - test_python_icat.get_session_details( - "session id", - client_pool=client_pool, - ) + with patch("icat.client.Client.getRemainingMinutes", return_value=-1), pytest.raises(AuthenticationError): + test_python_icat.get_session_details( + "session id", + client_pool=client_pool, + ) def test_valid_logout(self, test_client): client = Client( @@ -178,7 +181,7 @@ def test_valid_logout(self, test_client): headers=creds_header, ) - assert logout_response.status_code == 200 + assert logout_response.status_code == status.HTTP_200_OK def test_invalid_logout(self, bad_credentials_header, test_client): logout_response = test_client.delete( @@ -186,4 +189,4 @@ def test_invalid_logout(self, bad_credentials_header, test_client): headers=bad_credentials_header, ) - assert logout_response.status_code == 403 + assert logout_response.status_code == status.HTTP_403_FORBIDDEN diff --git a/test/integration/search_api/conftest.py b/test/integration/search_api/conftest.py index 6ccb9698..9d5aee75 100644 --- a/test/integration/search_api/conftest.py +++ b/test/integration/search_api/conftest.py @@ -1,8 +1,8 @@ import json from unittest.mock import mock_open, patch -from fastapi.testclient import TestClient import pytest +from fastapi.testclient import TestClient from datagateway_api.common.config import Config from datagateway_api.main import create_search_api_app diff --git a/test/integration/search_api/endpoints/test_count_dataset_files.py b/test/integration/search_api/endpoints/test_count_dataset_files.py index 653e556e..928cdb5b 100644 --- a/test/integration/search_api/endpoints/test_count_dataset_files.py +++ b/test/integration/search_api/endpoints/test_count_dataset_files.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status class TestSearchAPICountDatasetFilesEndpoint: @@ -58,7 +59,7 @@ def test_valid_count_dataset_files_endpoint( f"/Datasets/{pid}/files/count?where={request_filter}", ) - assert test_response.status_code == 200 + assert test_response.status_code == status.HTTP_200_OK assert test_response.json() == expected_json @pytest.mark.parametrize( @@ -77,4 +78,4 @@ def test_invalid_count_dataset_files_endpoint(self, test_search_api_client, pid, f"/Datasets/{pid}/files/count?where={request_filter}", ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/test/integration/search_api/endpoints/test_count_endpoint.py b/test/integration/search_api/endpoints/test_count_endpoint.py index b179cf3c..0e45b3d0 100644 --- a/test/integration/search_api/endpoints/test_count_endpoint.py +++ b/test/integration/search_api/endpoints/test_count_endpoint.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status class TestSearchAPICountEndpoint: @@ -84,7 +85,7 @@ def test_valid_count_endpoint( f"/{endpoint_name}/count?where={request_filter}", ) - assert test_response.status_code == 200 + assert test_response.status_code == status.HTTP_200_OK assert test_response.json() == expected_json @pytest.mark.parametrize( @@ -131,7 +132,7 @@ def test_valid_count_endpoint_is_public_field( f"/{endpoint_name}/count?where={request_filter}", ) - assert test_response.status_code == 200 + assert test_response.status_code == status.HTTP_200_OK assert test_response.json() == expected_json @pytest.mark.parametrize( @@ -153,4 +154,4 @@ def test_invalid_count_endpoint( f"/Datasets/count?where={request_filter}", ) - assert test_response.status_code == 400 + assert test_response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/test/integration/search_api/endpoints/test_get_dataset_files.py b/test/integration/search_api/endpoints/test_get_dataset_files.py index a06c4088..f776d566 100644 --- a/test/integration/search_api/endpoints/test_get_dataset_files.py +++ b/test/integration/search_api/endpoints/test_get_dataset_files.py @@ -1,6 +1,7 @@ from contextlib import suppress import pytest +from fastapi import status def prepare_data_for_assertion(response): @@ -12,14 +13,13 @@ def prepare_data_for_assertion(response): """ # Go through list and dicts to find dicts - for data in response: - if isinstance(response, dict): - data = response[data] + for item in response: + value = response[item] if isinstance(response, dict) else item - if type(data) in [dict, list]: - # Pass list and dicts back to the function to + if isinstance(value, (dict, list)): + # Pass lists and dicts back to the function to # loop through nested lists or dictionaries - prepare_data_for_assertion(data) + prepare_data_for_assertion(value) # This handles dictionaries passed to the function # Even if the dictionary doesn't contain creationDate @@ -158,25 +158,25 @@ def test_valid_get_dataset_files_endpoint( response_data = prepare_data_for_assertion(test_response.json()) - assert test_response.status_code == 200 + assert test_response.status_code == status.HTTP_200_OK assert response_data == expected_json @pytest.mark.parametrize( "pid, request_filter, expected_status_code", [ - pytest.param("0-8401-1070-7", '{"where": []}', 400, id="Bad where filter"), - pytest.param("0-8401-1070-7", '{"limit": -1}', 400, id="Bad limit filter"), - pytest.param("0-8401-1070-7", '{"skip": -100}', 400, id="Bad skip filter"), + pytest.param("0-8401-1070-7", '{"where": []}', status.HTTP_400_BAD_REQUEST, id="Bad where filter"), + pytest.param("0-8401-1070-7", '{"limit": -1}', status.HTTP_400_BAD_REQUEST, id="Bad limit filter"), + pytest.param("0-8401-1070-7", '{"skip": -100}', status.HTTP_400_BAD_REQUEST, id="Bad skip filter"), pytest.param( "0-8401-1070-7", '{"include": ""}', - 400, + status.HTTP_400_BAD_REQUEST, id="Bad include filter", ), pytest.param( "my 404 test pid", "{}", - 404, + status.HTTP_404_NOT_FOUND, id="Non-existent dataset pid", # Skipped because this actually returns 200 marks=pytest.mark.skip, diff --git a/test/integration/search_api/endpoints/test_get_entity_by_pid.py b/test/integration/search_api/endpoints/test_get_entity_by_pid.py index a507d9ce..48fe30e7 100644 --- a/test/integration/search_api/endpoints/test_get_entity_by_pid.py +++ b/test/integration/search_api/endpoints/test_get_entity_by_pid.py @@ -1,5 +1,5 @@ import pytest - +from fastapi import status from test.integration.search_api.endpoints.test_get_dataset_files import ( prepare_data_for_assertion, @@ -491,19 +491,19 @@ def test_valid_get_by_pid_endpoint( response_data = prepare_data_for_assertion(test_response.json()) - assert test_response.status_code == 200 + assert test_response.status_code == status.HTTP_200_OK assert response_data == expected_json @pytest.mark.parametrize( "pid, request_filter, expected_status_code", [ - pytest.param("0-8401-1070-7", '{"where": []}', 400, id="Bad where filter"), - pytest.param("0-8401-1070-7", '{"limit": -1}', 400, id="Bad limit filter"), - pytest.param("0-8401-1070-7", '{"skip": -100}', 400, id="Bad skip filter"), + pytest.param("0-8401-1070-7", '{"where": []}', status.HTTP_400_BAD_REQUEST, id="Bad where filter"), + pytest.param("0-8401-1070-7", '{"limit": -1}', status.HTTP_400_BAD_REQUEST, id="Bad limit filter"), + pytest.param("0-8401-1070-7", '{"skip": -100}', status.HTTP_400_BAD_REQUEST, id="Bad skip filter"), pytest.param( "0-8401-1070-7", '{"include": ""}', - 400, + status.HTTP_400_BAD_REQUEST, id="Bad include filter", ), pytest.param("my 404 test pid", "{}", 404, id="Non-existent dataset pid"), diff --git a/test/integration/search_api/endpoints/test_search_endpoint.py b/test/integration/search_api/endpoints/test_search_endpoint.py index b248d205..bd35d53c 100644 --- a/test/integration/search_api/endpoints/test_search_endpoint.py +++ b/test/integration/search_api/endpoints/test_search_endpoint.py @@ -1,5 +1,5 @@ import pytest - +from fastapi import status from test.integration.search_api.endpoints.test_get_dataset_files import ( prepare_data_for_assertion, @@ -729,8 +729,7 @@ class TestSearchAPISearchEndpoint: "parameters": [], }, ], - id="Search documents with datasets.parameters include and conditions" - " (between operator, A AND B AND C)", + id="Search documents with datasets.parameters include and conditions (between operator, A AND B AND C)", ), pytest.param( "Datasets", @@ -819,8 +818,7 @@ class TestSearchAPISearchEndpoint: { "name": "SAMPLE 4", "pid": "pid:4", - "description": "I read painting decade. Down" - " free attention recognize travel.", + "description": "I read painting decade. Down free attention recognize travel.", "datasets": [], }, ], @@ -839,8 +837,7 @@ class TestSearchAPISearchEndpoint: { "name": "SAMPLE 4", "pid": "pid:4", - "description": "I read painting decade. Down" - " free attention recognize travel.", + "description": "I read painting decade. Down free attention recognize travel.", "datasets": [], }, ], @@ -885,10 +882,10 @@ def test_valid_search_endpoint( @pytest.mark.parametrize( "request_filter, expected_status_code", [ - pytest.param('{"where": []}', 400, id="Bad where filter"), - pytest.param('{"limit": -1}', 400, id="Bad limit filter"), - pytest.param('{"skip": -100}', 400, id="Bad skip filter"), - pytest.param('{"include": ""}', 400, id="Bad include filter"), + pytest.param('{"where": []}', status.HTTP_400_BAD_REQUEST, id="Bad where filter"), + pytest.param('{"limit": -1}', status.HTTP_400_BAD_REQUEST, id="Bad limit filter"), + pytest.param('{"skip": -100}', status.HTTP_400_BAD_REQUEST, id="Bad skip filter"), + pytest.param('{"include": ""}', status.HTTP_400_BAD_REQUEST, id="Bad include filter"), ], ) def test_invalid_search_endpoint( diff --git a/test/integration/search_api/test_error_handling.py b/test/integration/search_api/test_error_handling.py index b4fe2faf..de8d6622 100644 --- a/test/integration/search_api/test_error_handling.py +++ b/test/integration/search_api/test_error_handling.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status from datagateway_api.common.exceptions import ( ApiError, @@ -16,21 +17,23 @@ class TestErrorHandling: @pytest.mark.parametrize( "raised_exception, expected_exception, status_code", [ - pytest.param(BadRequestError, BadRequestError, 400, id="Bad request error"), - pytest.param(FilterError, FilterError, 400, id="Invalid filter"), + pytest.param(BadRequestError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="Bad request error"), + pytest.param(FilterError, FilterError, status.HTTP_400_BAD_REQUEST, id="Invalid filter"), pytest.param( MissingRecordError, MissingRecordError, - 404, + status.HTTP_404_NOT_FOUND, id="Missing record", ), - pytest.param(ScoringAPIError, SearchAPIError, 500, id="Scoring API error"), - pytest.param(SearchAPIError, SearchAPIError, 500, id="Search API error"), - pytest.param(TypeError, BadRequestError, 400, id="Type error"), - pytest.param(ValueError, BadRequestError, 400, id="Value error"), - pytest.param(AttributeError, BadRequestError, 400, id="Attribute error"), - pytest.param(ImportError, ImportError, 500, id="Import error"), - pytest.param(Document, SearchAPIError, 500, id="Validation error"), + pytest.param( + ScoringAPIError, SearchAPIError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="Scoring API error" + ), + pytest.param(SearchAPIError, SearchAPIError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="Search API error"), + pytest.param(TypeError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="Type error"), + pytest.param(ValueError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="Value error"), + pytest.param(AttributeError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="Attribute error"), + pytest.param(ImportError, ImportError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="Import error"), + pytest.param(Document, SearchAPIError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="Validation error"), ], ) def test_valid_error_raised( diff --git a/test/integration/search_api/test_search_api_query.py b/test/integration/search_api/test_search_api_query.py index 7dd7653a..e22b6fb1 100644 --- a/test/integration/search_api/test_search_api_query.py +++ b/test/integration/search_api/test_search_api_query.py @@ -1,5 +1,5 @@ -from icat.query import Query import pytest +from icat.query import Query from datagateway_api.common.exceptions import SearchAPIError from datagateway_api.datagateway_api.icat.query import ICATQuery diff --git a/test/integration/search_api/test_session_handler.py b/test/integration/search_api/test_session_handler.py index 18bc5ac4..55ea057e 100644 --- a/test/integration/search_api/test_session_handler.py +++ b/test/integration/search_api/test_session_handler.py @@ -1,7 +1,7 @@ from datagateway_api.datagateway_api.icat.icat_client_pool import ICATClient from datagateway_api.search_api.session_handler import ( - client_manager, SessionHandler, + client_manager, ) diff --git a/test/integration/test_endpoint_rules.py b/test/integration/test_endpoint_rules.py index eaf53a63..a9ed4ae2 100644 --- a/test/integration/test_endpoint_rules.py +++ b/test/integration/test_endpoint_rules.py @@ -1,5 +1,5 @@ -from fastapi.routing import APIRoute, Mount import pytest +from fastapi.routing import APIRoute, Mount from datagateway_api.common.entity_endpoint_dict import endpoints @@ -34,7 +34,7 @@ class TestEndpointRules: def test_entity_endpoints(self, test_client, endpoint_ending, expected_methods): all_routes = collect_routes(test_client.app) - for endpoint_entity in endpoints.keys(): + for endpoint_entity in endpoints: endpoint_name = f"/{endpoint_entity.lower()}{endpoint_ending}" matching_routes = [methods for path, methods in all_routes if path == endpoint_name] diff --git a/test/integration/test_filter_order_handler.py b/test/integration/test_filter_order_handler.py index 91047ad9..2f7c0cf2 100644 --- a/test/integration/test_filter_order_handler.py +++ b/test/integration/test_filter_order_handler.py @@ -13,7 +13,7 @@ class TestFilterOrderHandler: """ `merge_python_icat_limit_skip_filters` and`clear_python_icat_order_filters()` are tested while testing the Python ICAT filters, so tests of these functions won't be - found here + found here. """ def test_add_filter(self, icat_query): diff --git a/test/integration/test_get_filters_from_query.py b/test/integration/test_get_filters_from_query.py index c08ec562..d6642ba3 100644 --- a/test/integration/test_get_filters_from_query.py +++ b/test/integration/test_get_filters_from_query.py @@ -1,5 +1,5 @@ -from fastapi import Request import pytest +from fastapi import Request from datagateway_api.common.helpers import get_filters_from_query_string from datagateway_api.datagateway_api.icat.filters import ( @@ -62,7 +62,7 @@ def test_valid_multiple_filters(self, test_client): @app.get("/test-multiple-filters/?limit=10&skip=4") def test_filters_route(request: Request): filters = get_filters_from_query_string(request, "datagateway_api") - assert len(filters) == 2 + assert len(filters) == 2 # noqa: PLR2004 test_client.get("/test-multiple-filters/?limit=10&skip=4") @@ -73,6 +73,6 @@ def test_valid_search_api_filter(self, test_client): @app.get('/test-search_api-filters/?filter={"skip": 5, "limit": 10}') def test_filters_route(request: Request): filters = get_filters_from_query_string(request, "search_api", "Dataset") - assert len(filters) == 2 + assert len(filters) == 2 # noqa: PLR2004 test_client.get('/test-search_api-filters/?filter={"skip": 5, "limit": 10}') diff --git a/test/integration/test_get_session_id_from_auth_header.py b/test/integration/test_get_session_id_from_auth_header.py index 60cb0367..e55b4dde 100644 --- a/test/integration/test_get_session_id_from_auth_header.py +++ b/test/integration/test_get_session_id_from_auth_header.py @@ -1,5 +1,4 @@ -from fastapi import Request - +from fastapi import Request, status from datagateway_api.common.exceptions import ( AuthenticationError, @@ -38,4 +37,4 @@ def test_headers_route(request: Request): # pylint:disable=unused-variable assert session_id == get_session_id_from_auth_header(request) response = local_auth_client.get("/", headers=valid_credentials_header) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK diff --git a/test/unit/search_api/test_models.py b/test/unit/search_api/test_models.py index 3b2f8a8d..c4f35d43 100644 --- a/test/unit/search_api/test_models.py +++ b/test/unit/search_api/test_models.py @@ -1,9 +1,9 @@ -from pydantic import ValidationError import pytest +from pydantic import ValidationError -import datagateway_api.search_api.models as models -from test.unit.search_api.utlis import normalise_date +from datagateway_api.search_api import models +from test.unit.search_api.utlis import normalise_date AFFILIATION_ICAT_DATA = { "id": 1, diff --git a/test/unit/search_api/test_panosc_mappings.py b/test/unit/search_api/test_panosc_mappings.py index 1b15cb7b..1934b55e 100644 --- a/test/unit/search_api/test_panosc_mappings.py +++ b/test/unit/search_api/test_panosc_mappings.py @@ -24,10 +24,7 @@ def test_invalid_load_mappings( test_config_without_search_api, search_api_config_flag, ): - if search_api_config_flag: - current_test_config = test_config - else: - current_test_config = test_config_without_search_api + current_test_config = test_config if search_api_config_flag else test_config_without_search_api with patch( "datagateway_api.common.config.Config.config", @@ -255,10 +252,8 @@ def test_get_icat_relations_for_non_related_fields_of_panosc_relation( test_entity_relation, expected_icat_relations, ): - icat_relations = ( - test_panosc_mappings.get_icat_relations_for_non_related_fields_of_panosc_relation( # noqa: B950 - test_panosc_entity_name, - test_entity_relation, - ) + icat_relations = test_panosc_mappings.get_icat_relations_for_non_related_fields_of_panosc_relation( # noqa: B950 + test_panosc_entity_name, + test_entity_relation, ) assert icat_relations == expected_icat_relations diff --git a/test/unit/search_api/test_search_scoring.py b/test/unit/search_api/test_search_scoring.py index b980b3e6..037fe3d3 100644 --- a/test/unit/search_api/test_search_scoring.py +++ b/test/unit/search_api/test_search_scoring.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from fastapi import status from requests import RequestException from datagateway_api.common.config import Config @@ -86,7 +87,7 @@ def test_get_score(self, post_mock): "group": Config.config.search_api.search_scoring.group, "limit": Config.config.search_api.search_scoring.limit, } - post_mock.return_value.status_code = 200 + post_mock.return_value.status_code = status.HTTP_200_OK post_mock.return_value.json.return_value = SEARCH_SCORING_API_SCORES_DATA scores = SearchScoring.get_score(scoring_query_filter_value) diff --git a/test/unit/search_api/utlis.py b/test/unit/search_api/utlis.py index 1c73265a..efed2683 100644 --- a/test/unit/search_api/utlis.py +++ b/test/unit/search_api/utlis.py @@ -12,14 +12,13 @@ class DateModel(BaseModel): ---------- date : SearchAPIDatetime A custom datetime type used by the Search API for consistent date handling. + """ date: SearchAPIDatetime def normalise_date(date_str: str): - """ - Convert a date string to the JSON-serializable 'date' field using DateModel. - """ + """Convert a date string to the JSON-serializable 'date' field using DateModel.""" dt_obj = DateHandler.str_to_datetime_object(date_str) return DateModel(date=dt_obj).model_dump(mode="json")["date"] diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 9054d40e..21af03f7 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -8,48 +8,42 @@ class TestAPIConfig: def test_load_with_no_config_data(self): - with patch("builtins.open", mock_open(read_data="{}")): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data="{}")), pytest.raises(SystemExit): + APIConfig.load("test/path") def test_load_with_missing_mandatory_config_data(self, test_config_data): del test_config_data["url_prefix"] - with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))), pytest.raises(SystemExit): + APIConfig.load("test/path") def test_load_with_datagateway_api_python_icat_and_missing_icat_config_data( self, test_config_data, ): del test_config_data["datagateway_api"]["icat_url"] - with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))), pytest.raises(SystemExit): + APIConfig.load("test/path") def test_load_with_invalid_api_extension_does_not_start_with_slash( self, test_config_data, ): test_config_data["datagateway_api"]["extension"] = "datagateway-api" - with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))), pytest.raises(SystemExit): + APIConfig.load("test/path") def test_load_with_invalid_api_extension_ends_with_slash( self, test_config_data, ): test_config_data["search_api"]["extension"] = "/search-api/" - with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))), pytest.raises(SystemExit): + APIConfig.load("test/path") def test_load_with_same_api_extensions(self, test_config_data): test_config_data["search_api"]["extension"] = "/datagateway-api" - with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))): - with pytest.raises(SystemExit): - APIConfig.load("test/path") + with patch("builtins.open", mock_open(read_data=json.dumps(test_config_data))), pytest.raises(SystemExit): + APIConfig.load("test/path") @pytest.mark.parametrize( "input_extension, expected_extension", diff --git a/test/unit/test_exceptions.py b/test/unit/test_exceptions.py index 9ea7af16..57a15b4b 100644 --- a/test/unit/test_exceptions.py +++ b/test/unit/test_exceptions.py @@ -1,4 +1,5 @@ import pytest +from fastapi import status from datagateway_api.common.exceptions import ( ApiError, @@ -53,17 +54,17 @@ def test_valid_exception_message(self, exception_class, expected_message): @pytest.mark.parametrize( "exception_class, expected_status_code", [ - pytest.param(ApiError, 500, id="ApiError"), - pytest.param(AuthenticationError, 403, id="AuthenticationError"), - pytest.param(BadRequestError, 400, id="BadRequestError"), - pytest.param(DatabaseError, 500, id="DatabaseError"), - pytest.param(FilterError, 400, id="FilterError"), - pytest.param(MissingCredentialsError, 401, id="MissingCredentialsError"), - pytest.param(MissingRecordError, 404, id="MissingRecordError"), - pytest.param(MultipleIncludeError, 400, id="MultipleIncludeError"), - pytest.param(PythonICATError, 500, id="PythonICATError"), - pytest.param(ScoringAPIError, 500, id="ScoringAPIError"), - pytest.param(SearchAPIError, 500, id="SearchAPIError"), + pytest.param(ApiError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="ApiError"), + pytest.param(AuthenticationError, status.HTTP_403_FORBIDDEN, id="AuthenticationError"), + pytest.param(BadRequestError, status.HTTP_400_BAD_REQUEST, id="BadRequestError"), + pytest.param(DatabaseError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="DatabaseError"), + pytest.param(FilterError, status.HTTP_400_BAD_REQUEST, id="FilterError"), + pytest.param(MissingCredentialsError, status.HTTP_401_UNAUTHORIZED, id="MissingCredentialsError"), + pytest.param(MissingRecordError, status.HTTP_404_NOT_FOUND, id="MissingRecordError"), + pytest.param(MultipleIncludeError, status.HTTP_400_BAD_REQUEST, id="MultipleIncludeError"), + pytest.param(PythonICATError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="PythonICATError"), + pytest.param(ScoringAPIError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="ScoringAPIError"), + pytest.param(SearchAPIError, status.HTTP_500_INTERNAL_SERVER_ERROR, id="SearchAPIError"), ], ) def test_valid_exception_status_code(self, exception_class, expected_status_code): diff --git a/test/unit/test_map_distinct_attrs.py b/test/unit/test_map_distinct_attrs.py index 5f5d9685..47f178bf 100644 --- a/test/unit/test_map_distinct_attrs.py +++ b/test/unit/test_map_distinct_attrs.py @@ -1,7 +1,7 @@ from datetime import datetime -from dateutil.tz import tzlocal import pytest +from dateutil.tz import tzlocal from datagateway_api.common.helpers import map_distinct_attributes_to_results diff --git a/test/unit/test_queries_records.py b/test/unit/test_queries_records.py index 1c653cb5..1aa98642 100644 --- a/test/unit/test_queries_records.py +++ b/test/unit/test_queries_records.py @@ -1,5 +1,6 @@ -from pydantic import ValidationError import pytest +from fastapi import status +from pydantic import ValidationError from datagateway_api.common.exceptions import ( BadRequestError, @@ -13,17 +14,17 @@ class TestQueriesRecords: @pytest.mark.parametrize( "raised_exception, expected_exception, status_code", [ - pytest.param(BadRequestError, BadRequestError, 400, id="bad request error"), - pytest.param(ValidationError, BadRequestError, 400, id="validation error"), - pytest.param(FilterError, FilterError, 400, id="invalid filter"), + pytest.param(BadRequestError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="bad request error"), + pytest.param(ValidationError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="validation error"), + pytest.param(FilterError, FilterError, status.HTTP_400_BAD_REQUEST, id="invalid filter"), pytest.param( MissingRecordError, MissingRecordError, - 404, + status.HTTP_404_NOT_FOUND, id="missing record", ), - pytest.param(TypeError, BadRequestError, 400, id="type error"), - pytest.param(ValueError, BadRequestError, 400, id="value error"), + pytest.param(TypeError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="type error"), + pytest.param(ValueError, BadRequestError, status.HTTP_400_BAD_REQUEST, id="value error"), ], ) def test_valid_error_raised( diff --git a/test/unit/test_query_filter.py b/test/unit/test_query_filter.py index ec865390..27ce7e07 100644 --- a/test/unit/test_query_filter.py +++ b/test/unit/test_query_filter.py @@ -3,8 +3,7 @@ class TestQueryFilter: def test_abstract_class(self): - """Test the `QueryFilter` class has all required abstract methods""" - + """Test the `QueryFilter` class has all required abstract methods.""" QueryFilter.__abstractmethods__ = set() class DummyQueryFilter(QueryFilter): diff --git a/util/icat_db_generator.py b/util/icat_db_generator.py index 91805a30..9a41787b 100644 --- a/util/icat_db_generator.py +++ b/util/icat_db_generator.py @@ -1,7 +1,7 @@ -from abc import ABC, abstractmethod import argparse import datetime import enum +from abc import ABC, abstractmethod from multiprocessing import Process from faker import Faker @@ -36,7 +36,8 @@ def get_date_time(): """ - Generates a datetime + Generates a datetime. + :return: the datetime """ return faker.date_time_between_dates( @@ -47,7 +48,8 @@ def get_date_time(): def get_start_date(i): """ - Generates a datetime from a number i + Generates a datetime from a number i. + :param i: :return: """ @@ -140,10 +142,10 @@ class DataCollectionGenerator(Generator): amount = 100 def generate(self): - for i in range(1, self.amount): - DataCollectionGenerator.generate_data_collection(self, i) + for _ in range(1, self.amount): + DataCollectionGenerator.generate_data_collection(self) - def generate_data_collection(self, i): + def generate_data_collection(self): data_collection = self.client.new("dataCollection") data_collection.doi = faker.isbn10(separator="-") data_collection.create() @@ -154,10 +156,10 @@ class FundingReferenceGenerator(Generator): amount = 100 def generate(self): - for i in range(1, self.amount): - FundingReferenceGenerator.generate_funding_reference(self, i) + for _ in range(1, self.amount): + FundingReferenceGenerator.generate_funding_reference(self) - def generate_funding_reference(self, i): + def generate_funding_reference(self): funding_reference = self.client.new("fundingReference") funding_reference.funderIdentifier = faker.ssn() funding_reference.funderName = faker.company() @@ -171,10 +173,10 @@ class TechniqueGenerator(Generator): amount = 100 def generate(self): - for i in range(1, self.amount): - TechniqueGenerator.generate_technique(self, i) + for _ in range(1, self.amount): + TechniqueGenerator.generate_technique(self) - def generate_technique(self, i): + def generate_technique(self): technique = self.client.new("technique") technique.pid = faker.word() technique.description = faker.text() @@ -688,10 +690,10 @@ class DataPublicationGenerator(Generator): amount = InvestigationGenerator.amount def generate(self): - for i in range(1, self.amount): - DataPublicationGenerator().generate_data_publication(i) + for _ in range(1, self.amount): + DataPublicationGenerator.generate_data_publication(self) - def generate_data_publication(self, i): + def generate_data_publication(self): data_publication = self.client.new("dataPublication") data_publication.title = faker.text() data_publication.description = faker.text() @@ -765,10 +767,10 @@ class DataPublicationTypeGenerator(Generator): amount = 20 def generate(self): - for i in range(1, self.amount): - DataPublicationTypeGenerator.generate_data_publication_type(self, i) + for _ in range(1, self.amount): + DataPublicationTypeGenerator.generate_data_publication_type(self) - def generate_data_publication_type(self, i): + def generate_data_publication_type(self): data_publication_type = self.client.new("dataPublicationType") data_publication_type.name = faker.word() data_publication_type.description = faker.text() @@ -1103,7 +1105,7 @@ def generate_datafile_parameter(cls, i): datafile_param.create() -def generate_all(i, generators, client): +def generate_all(i, generators): processes = [] for generator in generators: if generator.tier == i: @@ -1118,16 +1120,15 @@ def generate_all(i, generators, client): def main(): - client = icat_client() + icat_client() start_time = datetime.datetime.now() generators = [generator() for generator in Generator.__subclasses__()] tiers = 7 for i in range(tiers): - generate_all(i, generators, client) + generate_all(i, generators) print( - f"Added {sum(generator.amount for generator in generators)} entities in" - f" {datetime.datetime.now() - start_time}", + f"Added {sum(generator.amount for generator in generators)} entities in {datetime.datetime.now() - start_time}", ) diff --git a/util/setup_v11_1_0.py b/util/setup_v11_1_0.py index 96df5537..21ff129d 100644 --- a/util/setup_v11_1_0.py +++ b/util/setup_v11_1_0.py @@ -18,6 +18,7 @@ def get_password(password_file: "str | None") -> str: Returns: str: The user's password + """ if password_file is None: return getpass() @@ -28,7 +29,6 @@ def get_password(password_file: "str | None") -> str: def create( entity: Entity, - allow_existing: bool = False, fetch_existing: bool = False, ) -> Entity: try: @@ -81,7 +81,7 @@ def setup() -> None: parser.add_argument( "--allow-existing", action="store_true", - help=("Iterates over each Entity and if it already exists, suppresses the " "ICATObjectAlreadyExists error."), + help=("Iterates over each Entity and if it already exists, suppresses the ICATObjectAlreadyExists error."), ) args = parser.parse_args() @@ -117,7 +117,7 @@ def setup() -> None: if args.allow_existing: for entity in entities: - create(entity=entity, allow_existing=args.allow_existing) + create(entity=entity) else: client.createMany(entities) diff --git a/uv.lock b/uv.lock index 83dd48f8..a357fec0 100644 --- a/uv.lock +++ b/uv.lock @@ -28,58 +28,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "bandit" -version = "1.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "stevedore" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/72/f704a97aac430aeb704fa16435dfa24fbeaf087d46724d0965eb1f756a2c/bandit-1.9.2.tar.gz", hash = "sha256:32410415cd93bf9c8b91972159d5cf1e7f063a9146d70345641cd3877de348ce", size = 4241659, upload-time = "2025-11-23T21:36:18.722Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/1a/5b0320642cca53a473e79c7d273071b5a9a8578f9e370b74da5daa2768d7/bandit-1.9.2-py3-none-any.whl", hash = "sha256:bda8d68610fc33a6e10b7a8f1d61d92c8f6c004051d5e946406be1fb1b16a868", size = 134377, upload-time = "2025-11-23T21:36:17.39Z" }, -] - -[[package]] -name = "black" -version = "24.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468, upload-time = "2024-10-07T19:26:14.966Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270, upload-time = "2024-10-07T19:25:24.291Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061, upload-time = "2024-10-07T19:23:52.18Z" }, - { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293, upload-time = "2024-10-07T19:24:41.7Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" }, - { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" }, -] - [[package]] name = "cachetools" version = "4.2.4" @@ -301,42 +249,20 @@ dependencies = [ [package.dev-dependencies] code-analysis = [ - { name = "black" }, { name = "coverage" }, - { name = "flake8" }, - { name = "flake8-bandit" }, - { name = "flake8-black" }, - { name = "flake8-broken-line" }, - { name = "flake8-bugbear" }, - { name = "flake8-builtins" }, - { name = "flake8-commas" }, - { name = "flake8-comprehensions" }, - { name = "flake8-import-order" }, - { name = "flake8-logging-format" }, - { name = "pep8-naming" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-icdiff" }, + { name = "ruff" }, ] dev = [ - { name = "black" }, { name = "coverage" }, { name = "faker" }, - { name = "flake8" }, - { name = "flake8-bandit" }, - { name = "flake8-black" }, - { name = "flake8-broken-line" }, - { name = "flake8-bugbear" }, - { name = "flake8-builtins" }, - { name = "flake8-commas" }, - { name = "flake8-comprehensions" }, - { name = "flake8-import-order" }, - { name = "flake8-logging-format" }, - { name = "pep8-naming" }, { name = "pip-tools" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-icdiff" }, + { name = "ruff" }, ] scripts = [ { name = "faker" }, @@ -363,42 +289,20 @@ requires-dist = [ [package.metadata.requires-dev] code-analysis = [ - { name = "black", specifier = ">=24.10.0,<25" }, { name = "coverage", specifier = ">=7.6.4,<8" }, - { name = "flake8", specifier = ">=7.1.1,<8" }, - { name = "flake8-bandit", specifier = ">=4.1.1,<5" }, - { name = "flake8-black", specifier = ">=0.3.6,<0.4" }, - { name = "flake8-broken-line", specifier = ">=1.0.0,<2" }, - { name = "flake8-bugbear", specifier = ">=24.8.19,<25" }, - { name = "flake8-builtins", specifier = ">=2.5.0,<3" }, - { name = "flake8-commas", specifier = ">=4.0.0,<5" }, - { name = "flake8-comprehensions", specifier = ">=3.15.0,<4" }, - { name = "flake8-import-order", specifier = ">=0.18.2,<0.19" }, - { name = "flake8-logging-format", specifier = ">=1.0.0,<2" }, - { name = "pep8-naming", specifier = ">=0.14.1,<0.15" }, { name = "pytest", specifier = ">=8.3.3,<9" }, { name = "pytest-cov", specifier = ">=5.0.0,<6" }, { name = "pytest-icdiff", specifier = ">=0.9,<0.10" }, + { name = "ruff", specifier = ">=0.15.16" }, ] dev = [ - { name = "black", specifier = ">=24.10.0,<25" }, { name = "coverage", specifier = ">=7.6.4,<8" }, { name = "faker", specifier = ">=8.5.1" }, - { name = "flake8", specifier = ">=7.1.1,<8" }, - { name = "flake8-bandit", specifier = ">=4.1.1,<5" }, - { name = "flake8-black", specifier = ">=0.3.6,<0.4" }, - { name = "flake8-broken-line", specifier = ">=1.0.0,<2" }, - { name = "flake8-bugbear", specifier = ">=24.8.19,<25" }, - { name = "flake8-builtins", specifier = ">=2.5.0,<3" }, - { name = "flake8-commas", specifier = ">=4.0.0,<5" }, - { name = "flake8-comprehensions", specifier = ">=3.15.0,<4" }, - { name = "flake8-import-order", specifier = ">=0.18.2,<0.19" }, - { name = "flake8-logging-format", specifier = ">=1.0.0,<2" }, - { name = "pep8-naming", specifier = ">=0.14.1,<0.15" }, { name = "pip-tools", specifier = ">=5.3.1" }, { name = "pytest", specifier = ">=8.3.3,<9" }, { name = "pytest-cov", specifier = ">=5.0.0,<6" }, { name = "pytest-icdiff", specifier = ">=0.9,<0.10" }, + { name = "ruff", specifier = ">=0.15.16" }, ] scripts = [ { name = "faker", specifier = ">=8.5.1" }, @@ -610,128 +514,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, ] -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - -[[package]] -name = "flake8-bandit" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bandit" }, - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/1c/4f66a7a52a246d6c64312b5c40da3af3630cd60b27af81b137796af3c0bc/flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e", size = 5403, upload-time = "2022-08-29T13:48:41.225Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/5f/55bab0ac89f9ad9f4c6e38087faa80c252daec4ccb7776b4dac216ca9e3f/flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d", size = 4828, upload-time = "2022-08-29T13:48:39.737Z" }, -] - -[[package]] -name = "flake8-black" -version = "0.3.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "black" }, - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/0f/be40887d6d9722528a5716cfaf73f1ea6ff2cc7bc77fa251e76fc87df2ee/flake8_black-0.3.7.tar.gz", hash = "sha256:a77f46d9b20414749d29a8b3c3abdc367b7b41ea977e55629467304c37c3b6ba", size = 13338, upload-time = "2025-09-19T14:56:40.185Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/e4/4a53c1ed4cd309e804c14cbce9494c2a922db8a14b6c166283637c29e310/flake8_black-0.3.7-py3-none-any.whl", hash = "sha256:366215fbe9ee3d5a7e72710da9be871f52392e568ebd2821a16cd5f3a046d291", size = 9840, upload-time = "2025-09-19T14:56:39.253Z" }, -] - -[[package]] -name = "flake8-broken-line" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/5e/eca08446205afb79e74b6af8e227f06f0b1a26ae892708adbc4e65ccaa86/flake8_broken_line-1.0.0.tar.gz", hash = "sha256:e2c6a17f8d9a129e99c1320fce89b33843e2963871025c4c2bb7b8b8d8732a85", size = 3458, upload-time = "2023-05-31T10:09:11.716Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/ff/57d0101933527b5202cc9f80bc15aa85b207916c722a00e7adde0e33f413/flake8_broken_line-1.0.0-py3-none-any.whl", hash = "sha256:96c964336024a5030dc536a9f6fb02aa679e2d2a6b35b80a558b5136c35832a9", size = 4202, upload-time = "2023-05-31T10:09:10.027Z" }, -] - -[[package]] -name = "flake8-bugbear" -version = "24.12.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/25/48ba712ff589b0149f21135234f9bb45c14d6689acc6151b5e2ff8ac2ae9/flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64", size = 82907, upload-time = "2024-12-12T16:49:26.307Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/21/0a875f75fbe4008bd171e2fefa413536258fe6b4cfaaa087986de74588f4/flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e", size = 36664, upload-time = "2024-12-12T16:49:23.584Z" }, -] - -[[package]] -name = "flake8-builtins" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/33/fdfe44695094124e0e518fdd4fed0c7a34a5920de7063babd4b2bbdc8a7f/flake8_builtins-2.5.0.tar.gz", hash = "sha256:bdaa3dd823e4f5308c5e712d19fa5f69daa52781ea874f5ea9c3637bcf56faa6", size = 16499, upload-time = "2024-04-09T10:57:33.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/33/e423d57dbe2a2ad664d166aeceac58b469510d6c763fada32afd47ffc9ef/flake8_builtins-2.5.0-py3-none-any.whl", hash = "sha256:8cac7c52c6f0708c0902b46b385bc7e368a9068965083796f1431c0d2e6550cf", size = 11964, upload-time = "2024-04-09T10:57:31.271Z" }, -] - -[[package]] -name = "flake8-commas" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/e7/4add4247c9ea7871f6bc9f794f2771647b809bb492b36c8ce18825d64994/flake8_commas-4.0.0.tar.gz", hash = "sha256:a68834b42a9a31c94ca790efe557a932c0eae21a3479c6b9a23c4dc077e3ea96", size = 9338, upload-time = "2024-05-16T20:07:46.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d4/75d1cdd7c89cb6af4f956393c339bb9a7d203999432a823f60b64c0ea072/flake8_commas-4.0.0-py3-none-any.whl", hash = "sha256:cad476d71ba72e8b941a8508d5b9ffb6b03e50f7102982474f085ad0d674b685", size = 7877, upload-time = "2024-05-16T20:07:44.536Z" }, -] - -[[package]] -name = "flake8-comprehensions" -version = "3.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/93/da2d30987ce4c06404326e7c8e6636cae18c2b2f2d5542891a8ad64a34f4/flake8_comprehensions-3.17.0.tar.gz", hash = "sha256:bf4fa102b2bf4d6c9e999e29e4b2724cbadffb70b937600fc782161be4dc0f4a", size = 13269, upload-time = "2025-09-09T22:37:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/bd/d6739d685fdd79349aa51c37bdedc0d8eab6ae9c6e6ed2ca935b3f88210d/flake8_comprehensions-3.17.0-py3-none-any.whl", hash = "sha256:3943a9c6f2593c3bc5cc64106c2f89d63c6ecd49c8343597f8257b8fcfc8b0a2", size = 8185, upload-time = "2025-09-09T22:37:17.149Z" }, -] - -[[package]] -name = "flake8-import-order" -version = "0.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/55/181d5e1b70ff1c7e1ee1d5e2247dfd53f1168fe706656f57e788aa1df2ab/flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e", size = 21717, upload-time = "2022-11-26T20:14:25.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/52/9c5cb50a61f3d90f9d6c98ba67e3227f4057dc398cf664f3b56cb7c261f7/flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df", size = 15971, upload-time = "2022-11-26T20:14:23.899Z" }, -] - -[[package]] -name = "flake8-logging-format" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/8d/dd934e9b912c0ad28b4549a0e5f334492662e494b1852e2da32d474da258/flake8_logging_format-1.0.0-py3-none-any.whl", hash = "sha256:2817f44352e8bfa98a555ef1c529ddecd09acd6aea82ade0c1ea314cee458178", size = 11451, upload-time = "2024-06-11T16:56:47.68Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -939,15 +721,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -957,15 +730,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "orjson" version = "3.11.5" @@ -1043,27 +807,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, -] - -[[package]] -name = "pep8-naming" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flake8" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/8e/1de32e908d8b008bb9352bfe7749aedecb71e2793d36c7ee342716acd1ec/pep8-naming-0.14.1.tar.gz", hash = "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36", size = 16546, upload-time = "2024-05-17T14:08:44.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/a2/450b71d1a87fcee50a7b994a53b1c68fc6a6b718df0eb035f2bffb2d3a4f/pep8_naming-0.14.1-py3-none-any.whl", hash = "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5", size = 8859, upload-time = "2024-05-17T14:08:42.738Z" }, -] - [[package]] name = "pip" version = "25.3" @@ -1087,15 +830,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/d5/c0f282060c483e6fd4635ed8f4f88aff02c5490d0fc588edec52b0cb9d7b/pip_tools-5.3.1-py2.py3-none-any.whl", hash = "sha256:73787e23269bf8a9230f376c351297b9037ed0d32ab0f9bef4a187d976acc054", size = 45159, upload-time = "2020-07-31T14:44:12.152Z" }, ] -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1123,15 +857,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/cd/6ca8bcf55c71219428b2d2734303aec6137afdf372284e79ee21686b1462/py_object_pool-1.1-py3-none-any.whl", hash = "sha256:9be717f00b861bbecc45f38108a96d7251bcaba4e02b24bbcc5115ffb9d32104", size = 14034, upload-time = "2020-02-09T14:26:37.671Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pydantic" version = "2.12.5" @@ -1276,15 +1001,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, ] -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -1569,6 +1285,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "sentry-sdk" version = "2.49.0" @@ -1582,15 +1323,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/43/1c586f9f413765201234541857cb82fda076f4b0f7bad4a0ec248da39cf3/sentry_sdk-2.49.0-py2.py3-none-any.whl", hash = "sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0", size = 415693, upload-time = "2026-01-08T09:56:21.872Z" }, ] -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -1621,15 +1353,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225, upload-time = "2024-11-18T19:45:02.027Z" }, ] -[[package]] -name = "stevedore" -version = "5.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, -] - [[package]] name = "suds" version = "1.2.0"