From f452b5b67f84cbdedad2f917a057ed8ba9d65ce5 Mon Sep 17 00:00:00 2001 From: Krzysztof Magusiak Date: Tue, 7 May 2024 12:11:20 +0200 Subject: [PATCH] Update to python 3.9 --- .devcontainer/devcontainer.json | 2 +- .github/workflows/python-lint-test.yml | 10 ++--- .github/workflows/python-publish.yml | 4 +- odoo_connect/__init__.py | 6 +-- odoo_connect/attachment.py | 14 +++--- odoo_connect/data.py | 59 +++++++++++++------------- odoo_connect/explore.py | 33 +++++++------- odoo_connect/format.py | 18 ++++---- odoo_connect/odoo_rpc.py | 38 +++++++++-------- pyproject.toml | 6 +-- tests/mock_odoo_server.py | 6 +-- 11 files changed, 100 insertions(+), 96 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 58842c8..5ab882c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Odoo Connect", - "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.7", + "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", "customizations": { "vscode": { diff --git a/.github/workflows/python-lint-test.yml b/.github/workflows/python-lint-test.yml index eed4233..dccbdaf 100644 --- a/.github/workflows/python-lint-test.yml +++ b/.github/workflows/python-lint-test.yml @@ -6,9 +6,9 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies @@ -23,11 +23,11 @@ jobs: strategy: matrix: # lowest, common (default ubuntu LTS), newest - python-version: ["3.7", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index dfbfdf1..0cf9550 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -21,12 +21,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # don't use shallow checkout to determine an intermediary version correctly fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies diff --git a/odoo_connect/__init__.py b/odoo_connect/__init__.py index a31e36c..aa909df 100644 --- a/odoo_connect/__init__.py +++ b/odoo_connect/__init__.py @@ -1,6 +1,6 @@ import logging import urllib.parse -from typing import Dict, Optional +from typing import Optional from .odoo_rpc import OdooClient, OdooModel, OdooServerError # noqa @@ -20,7 +20,7 @@ def connect( password: Optional[str] = None, infer_parameters: bool = True, check_connection: bool = True, - context: Optional[Dict] = None, + context: Optional[dict] = None, monodb: bool = False, **kw, ) -> OdooClient: @@ -105,7 +105,7 @@ def connect( return client except (NotImplementedError, OdooConnectionError): raise - except (ConnectionError, IOError, OdooServerError) as e: + except (ConnectionError, OSError, OdooServerError) as e: raise OdooConnectionError(e) diff --git a/odoo_connect/attachment.py b/odoo_connect/attachment.py index 96e57ee..89404c4 100644 --- a/odoo_connect/attachment.py +++ b/odoo_connect/attachment.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, List, Optional, overload +from typing import Optional, overload from .format import decode_binary from .odoo_rpc import OdooClient, OdooModel, urljoin @@ -47,7 +47,7 @@ def get_attachment( if field_info.get("relation") == "ir.attachment": if field_info.get("type") != "many2one": raise RuntimeError( - "Field %s is not a many2one, here are the values: %s" % (field_name, value) + f"Field {field_name} is not a many2one, here are the values: {value}" ) if not value: return b'' @@ -59,7 +59,7 @@ def get_attachment( return decode_binary(value) -def get_attachments(odoo: OdooClient, ids: List[int]) -> Dict[int, bytes]: +def get_attachments(odoo: OdooClient, ids: list[int]) -> dict[int, bytes]: """Get a list of tuples (name, raw_bytes) from ir.attachment by ids :param odoo: Odoo client @@ -71,8 +71,8 @@ def get_attachments(odoo: OdooClient, ids: List[int]) -> Dict[int, bytes]: def list_attachments( - model: OdooModel, ids: List[int], *, domain=[], fields=[], generate_access_token=False -) -> List[Dict]: + model: OdooModel, ids: list[int], *, domain=[], fields=[], generate_access_token=False +) -> list[dict]: """List attachments We search all attachments linked to the model and ids. @@ -140,7 +140,7 @@ def attachment_url(d, _model_name=None, _id=None): def _download_content( - odoo: OdooClient, url: str, *, params: Dict = {}, access_token: Optional[str] = None + odoo: OdooClient, url: str, *, params: dict = {}, access_token: Optional[str] = None ) -> bytes: """Download contents from a URL""" log = logging.getLogger(__name__) @@ -198,7 +198,7 @@ def download_image( # REPORTS -def list_reports(model: OdooModel) -> List[Dict]: +def list_reports(model: OdooModel) -> list[dict]: """List of reports from a model""" return model.odoo['ir.actions.report'].search_read( [('model', '=', model.model)], ['name', 'report_type', 'report_name'] diff --git a/odoo_connect/data.py b/odoo_connect/data.py index 8cb436a..eed6f57 100644 --- a/odoo_connect/data.py +++ b/odoo_connect/data.py @@ -1,7 +1,8 @@ import json import logging +from collections.abc import Iterable, Iterator from dataclasses import dataclass -from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, cast +from typing import Any, Optional, cast from .format import Formatter from .odoo_rpc import OdooModel, urljoin @@ -16,8 +17,8 @@ def make_batches( - data: Iterable[Dict], *, batch_size: int = 1000, group_by: str = '' -) -> Iterable[List[Dict]]: + data: Iterable[dict], *, batch_size: int = 1000, group_by: str = '' +) -> Iterable[list[dict]]: """Split an interable to a batched iterable :param data: The iterable @@ -25,7 +26,7 @@ def make_batches( :param group_by: field to group by (the value is kept in a single batch, can be empty) :return: An iterable of batches """ - batch: List[Dict] = [] + batch: list[dict] = [] if group_by: data = sorted(data, key=lambda d: d[group_by]) group_value = None @@ -53,7 +54,7 @@ def load_data( *, method: str = "load", method_row_type=None, - fields: Optional[List[str]] = None, + fields: Optional[list[str]] = None, formatter: Optional[Formatter] = None, ): """Load the data into the model. @@ -100,7 +101,7 @@ def load_data( log.info("Load data using %s.%s(), %d records", model.model, method, len(data)) if method == 'load': - fields = [f.replace('.', '/') for f in cast(List[str], fields)] + fields = [f.replace('.', '/') for f in cast(list[str], fields)] return model.execute(method, fields=fields, data=data) if method == 'write': return __load_data_write(model, data) @@ -110,8 +111,8 @@ def load_data( def __convert_to_type_list( - data: Iterable, fields: Optional[List[str]] -) -> Tuple[Iterable[List], List[str]]: + data: Iterable, fields: Optional[list[str]] +) -> tuple[Iterable[list], list[str]]: idata = iter(data) first_row = next(idata, None) if first_row is None: @@ -129,7 +130,7 @@ def __convert_to_type_list( return data, fields -def __convert_to_type_dict(data: Iterable, fields: Optional[List[str]]) -> Iterable[Dict]: +def __convert_to_type_dict(data: Iterable, fields: Optional[list[str]]) -> Iterable[dict]: idata = iter(data) first_row = next(idata, None) if first_row is None: @@ -148,7 +149,7 @@ def __convert_to_type_dict(data: Iterable, fields: Optional[List[str]]) -> Itera return data -def __load_data_write(model: OdooModel, data: List[Dict]) -> Dict: +def __load_data_write(model: OdooModel, data: list[dict]) -> dict: """Use multiple write() and create() calls to update data :return: {'write_count': x, 'create_count': x, 'ids': [list of ids]} @@ -192,19 +193,19 @@ def __str__(self) -> str: class ExportData: """Exported data from Odoo""" - schema: List[Dict] - data: List[List] + schema: list[dict] + data: list[list] @property def column_names(self): return [h['name'] for h in self.schema] - def to_dicts(self) -> Iterable[Dict]: + def to_dicts(self) -> Iterable[dict]: """Return the data as dicts""" fields = self.column_names return (dict(zip(fields, d)) for d in self.data) - def to_csv(self, with_header=True) -> Iterable[List[str]]: + def to_csv(self, with_header=True) -> Iterable[list[str]]: """Return the data for writing a csv file""" if with_header: yield self.column_names @@ -244,7 +245,7 @@ def to_dbapi(self, con, table_name: str, *, only_data: bool = False, drop: bool self.data, ) - def get_sql_columns(self) -> List[ColumnSpec]: + def get_sql_columns(self) -> list[ColumnSpec]: """Get the list of tuples (normalized_column_name, column_type) to write into a table""" type_to_sql = { 'binary': 'binary', @@ -275,7 +276,7 @@ def __str__(self) -> str: return f"ExportData{self.column_names}({len(self.data)} rows)" -def fields_from_export(model: OdooModel, export_name: str) -> List[str]: +def fields_from_export(model: OdooModel, export_name: str) -> list[str]: """Return the list of fields in ir.exports""" fields = model.odoo['ir.exports.line'].search_read( [ @@ -290,7 +291,7 @@ def fields_from_export(model: OdooModel, export_name: str) -> List[str]: return fields -def domain_from_filter(model: OdooModel, filter_name: str) -> Dict: +def domain_from_filter(model: OdooModel, filter_name: str) -> dict: """Return a tuple (domain, kwargs for search) from a filter name""" filter_data = model.odoo['ir.filters'].search_read_dict( [ @@ -311,8 +312,8 @@ def domain_from_filter(model: OdooModel, filter_name: str) -> Dict: def export_data( model: OdooModel, - domain: List, - fields: List[str], + domain: list, + fields: list[str], *, formatter: Optional[Formatter] = None, expand_many: bool = False, @@ -357,7 +358,7 @@ def export_data( def add_fields( - model: OdooModel, data: List[Dict], by_field: str, fields: List[str] = ['id'], domain: List = [] + model: OdooModel, data: list[dict], by_field: str, fields: list[str] = ['id'], domain: list = [] ): """Add fields by querying the model @@ -368,12 +369,12 @@ def add_fields( :param domain: Additional domain to use :raises Exception: When multiple results have the same by_field key """ - domain_by_field: List[Any] = [(by_field, 'in', [d[by_field] for d in data])] + domain_by_field: list[Any] = [(by_field, 'in', [d[by_field] for d in data])] domain = ['&'] + domain_by_field + (domain if domain else domain_by_field) fetched_data = model.search_read_dict(domain, fields + [by_field]) index = {d.pop(by_field): d for d in fetched_data} if len(index) != len(fetched_data): - raise Exception('%s is not unique in %s when adding fields' % (by_field, model.model)) + raise Exception(f'{by_field} is not unique in {model.model} when adding fields') for d in data: updates = index.get(d[by_field]) if updates: @@ -382,7 +383,7 @@ def add_fields( d.update({f: None for f in fields}) -def add_xml_id(model: OdooModel, data: List, *, id_name='id', xml_id_field='xml_id'): +def add_xml_id(model: OdooModel, data: list, *, id_name='id', xml_id_field='xml_id'): """Add a field containg the xml_id :param model: The model @@ -427,7 +428,7 @@ def add_xml_id(model: OdooModel, data: List, *, id_name='id', xml_id_field='xml_ def add_url( model: OdooModel, - data: List, + data: list, *, url_field='url', model_id_func=None, @@ -463,7 +464,7 @@ def add_url( raise TypeError('Cannot append the url to %s' % type(row)) -def _flatten(value, access: List[str]) -> Any: +def _flatten(value, access: list[str]) -> Any: if not access: return value if isinstance(value, dict): @@ -473,7 +474,7 @@ def _flatten(value, access: List[str]) -> Any: return False # default -def _expand_many(data: Iterable[Dict]) -> Iterator[Dict]: +def _expand_many(data: Iterable[dict]) -> Iterator[dict]: for d in data: should_yield = True for k, v in d.items(): @@ -491,12 +492,12 @@ def _expand_many(data: Iterable[Dict]) -> Iterator[Dict]: def flatten( - data: Iterable[Dict], - fields: List[str], + data: Iterable[dict], + fields: list[str], *, formatter: Optional[Formatter] = None, expand_many: bool = False, -) -> Iterator[List]: +) -> Iterator[list]: """Flatten each dict with values into a single row""" if expand_many: data = _expand_many(data) diff --git a/odoo_connect/explore.py b/odoo_connect/explore.py index 1a5c93e..aec6d79 100644 --- a/odoo_connect/explore.py +++ b/odoo_connect/explore.py @@ -1,5 +1,6 @@ +from collections.abc import Iterable from contextvars import ContextVar -from typing import Any, Callable, Dict, Iterable, List, Union +from typing import Any, Callable, Union import odoo_connect.format @@ -9,7 +10,7 @@ """Cache of read values""" -GLOBAL_CACHE: ContextVar[Dict[odoo_rpc.OdooModel, Dict[int, Dict[str, Any]]]] = ContextVar( +GLOBAL_CACHE: ContextVar[dict[odoo_rpc.OdooModel, dict[int, dict[str, Any]]]] = ContextVar( "OdooExploreCache", default={} ) @@ -18,9 +19,9 @@ class Instance: """A proxy for an instance set""" __model: odoo_rpc.OdooModel - __ids: List[int] + __ids: list[int] - def __init__(self, model: odoo_rpc.OdooModel, ids: List[int]) -> None: + def __init__(self, model: odoo_rpc.OdooModel, ids: list[int]) -> None: self.__model = model self.__ids = ids @@ -60,7 +61,7 @@ def __and__(self, other) -> "Instance": return Instance(self.__model, ids) @property - def ids(self) -> List[int]: + def ids(self) -> list[int]: return self.__ids @property @@ -119,7 +120,7 @@ def mapped(self, path: str): return value.mapped(paths[1]) return self._mapped(path) - def _mapped(self, field_name: str) -> Union["Instance", List[Any]]: + def _mapped(self, field_name: str) -> Union["Instance", list[Any]]: """Map/read a field""" prop = self.__model.fields().get(field_name) if not prop: @@ -130,17 +131,17 @@ def _mapped(self, field_name: str) -> Union["Instance", List[Any]]: model = self.__model.odoo.get_model(relation) # value is either list[int] (many) or int|False (one) lists = (v if isinstance(v, list) else [v] for v in values if v) - ids = set(i for ls in lists for i in ls) + ids = {i for ls in lists for i in ls} return Instance(model, list(ids)) return values - def cache(self, fields: List[str] = [], computed=False, exists=False) -> "Instance": + def cache(self, fields: list[str] = [], computed=False, exists=False) -> "Instance": """Cache the record fields and return self""" fieldset = set(self._default_fields(computed=computed) + fields) model_cache = self.__cache() # find missing ids, when missing in cache or field missing in cache # read all at once to have more consistency and avoid roundtrips - missing_ids = set(i for i in self.__ids if fieldset - model_cache.get(i, {}).keys()) + missing_ids = {i for i in self.__ids if fieldset - model_cache.get(i, {}).keys()} # an exists() check is not needed because read() will return only existing rows if not missing_ids: return self @@ -152,7 +153,7 @@ def cache(self, fields: List[str] = [], computed=False, exists=False) -> "Instan model_cache[id] = d return self - def read(self, *, check_fields: List[str] = []) -> List[Dict[str, Any]]: + def read(self, *, check_fields: list[str] = []) -> list[dict[str, Any]]: """Read the data""" self.cache(fields=check_fields) model_cache = self.__cache() @@ -161,7 +162,7 @@ def read(self, *, check_fields: List[str] = []) -> List[Dict[str, Any]]: except KeyError as e: raise odoo_rpc.OdooServerError(f"Cannot read {self.__model.model}: {e}") - def _default_fields(self, *, computed=False) -> List[str]: + def _default_fields(self, *, computed=False) -> list[str]: """List of fields to read by default""" data = self.__model.fields() return [ @@ -172,7 +173,7 @@ def _default_fields(self, *, computed=False) -> List[str]: and (computed or f in ('id', 'display_name') or prop.get('store')) ] - def fields_get(self, field_names=[]) -> Dict[str, Dict]: + def fields_get(self, field_names=[]) -> dict[str, dict]: """Get the field information""" data = self.__model.fields() if field_names: @@ -192,7 +193,7 @@ def exists(self) -> "Instance": ids = set(self.__ids) & model_cache.keys() return self.browse(*ids) if len(ids) < len(self.__ids) else self - def search(self, domain: List, **kw) -> "Instance": + def search(self, domain: list, **kw) -> "Instance": """Search for an instance""" fields = self._default_fields() data = self.__model._search_read(domain, fields, **kw) @@ -207,7 +208,7 @@ def name_search(self, name: str, **kw) -> "Instance": data = self.__model.name_search(name, **kw) return Instance(self.__model, [d[0] for d in data]) - def create(self, *values: Dict[str, Any], format: bool = False) -> "Instance": + def create(self, *values: dict[str, Any], format: bool = False) -> "Instance": """Create multiple instances""" if not values: return self.browse() @@ -219,7 +220,7 @@ def create(self, *values: Dict[str, Any], format: bool = False) -> "Instance": ids = self.__model.create(value_list) return self.browse(*ids) - def write(self, values: Dict[str, Any], *, format: bool = False): + def write(self, values: dict[str, Any], *, format: bool = False): """Update the values of the current instance""" if not values: return @@ -238,7 +239,7 @@ def copy(self): ids = self.__model.copy(self.__ids) return self.browse(*ids) - def __cache(self) -> Dict[int, Dict[str, Any]]: + def __cache(self) -> dict[int, dict[str, Any]]: cache = GLOBAL_CACHE.get() model_cache = cache.get(self.__model) if not model_cache: diff --git a/odoo_connect/format.py b/odoo_connect/format.py index a2ce22e..3ddcb2c 100644 --- a/odoo_connect/format.py +++ b/odoo_connect/format.py @@ -2,7 +2,7 @@ from collections import defaultdict from contextvars import ContextVar from datetime import date, datetime, timezone -from typing import Any, Callable, Dict, Optional, Tuple, Union, cast +from typing import Any, Callable, Optional, Union, cast from .odoo_rpc import OdooModel @@ -24,7 +24,7 @@ } """Default formatters for models""" -DEFAULT_FORMATTERS: ContextVar[Dict[OdooModel, "Formatter"]] = ContextVar( +DEFAULT_FORMATTERS: ContextVar[dict[OdooModel, "Formatter"]] = ContextVar( 'OdooDefaultFormatters', default={} ) @@ -124,7 +124,7 @@ def format_binary(v: Union[bytes, str]) -> str: """Transform type to tuple(formatter, decoder)""" -_FORMAT_FUNCTIONS: Dict[str, Tuple[Callable, Callable]] = { +_FORMAT_FUNCTIONS: dict[str, tuple[Callable, Callable]] = { 'datetime': (format_datetime, decode_datetime), 'date': (format_date, decode_date), 'binary': (format_binary, decode_binary), @@ -142,10 +142,10 @@ class Formatter: """Transformations to apply to source fields. Use an empty string to mask some of them.""" model: Optional[OdooModel] = None - field_map: Dict[str, str] - field_info: Dict[str, Dict] - format_function: Dict[str, Callable[[Any], Any]] - decode_function: Dict[str, Callable[[Any], Any]] + field_map: dict[str, str] + field_info: dict[str, dict] + format_function: dict[str, Callable[[Any], Any]] + decode_function: dict[str, Callable[[Any], Any]] lower_case_fields: bool = False def __init__( @@ -200,12 +200,12 @@ def map_field_name(self, name: str) -> str: name = name.lower() return name - def format_dict(self, d: Dict[str, Any]) -> Dict[str, Any]: + def format_dict(self, d: dict[str, Any]) -> dict[str, Any]: """Apply formatting to each field (python object to Odoo data)""" d_renamed = [(self.map_field_name(f), v) for f, v in d.items()] return {f: self.format_function[f](v) for f, v in d_renamed if f} - def decode_dict(self, d: Dict[str, Any]) -> Dict[str, Any]: + def decode_dict(self, d: dict[str, Any]) -> dict[str, Any]: """Apply decoding to each field (Odoo data to python object)""" return {f: self.decode_function[f](v) for f, v in d.items()} diff --git a/odoo_connect/odoo_rpc.py b/odoo_connect/odoo_rpc.py index 037a8c4..78d186f 100644 --- a/odoo_connect/odoo_rpc.py +++ b/odoo_connect/odoo_rpc.py @@ -1,7 +1,7 @@ import logging import random import re -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import requests @@ -54,13 +54,13 @@ class OdooClient: """Odoo server connection""" url: str - _models: Dict[str, "OdooModel"] - _version: Dict[str, Any] + _models: dict[str, "OdooModel"] + _version: dict[str, Any] _database: str _username: str _password: str _uid: Optional[int] - context: Dict + context: dict def __init__( self, @@ -194,18 +194,18 @@ def get_model(self, model_name: str, check: bool = False) -> "OdooModel": raise OdooServerError('Model %s not found' % model) return model - def list_databases(self) -> List[str]: + def list_databases(self) -> list[str]: """Get the list of databases (may be disabled on the server and fail)""" return self._call("db", "list") - def list_models(self) -> List[str]: + def list_models(self) -> list[str]: """Get the list of known model names.""" models = self.get_model('ir.model').search_read([], ['model']) return [m['model'] for m in models] def ref( - self, xml_id: str, fields: List[str] = [], raise_if_not_found: bool = True - ) -> Optional[Dict]: + self, xml_id: str, fields: list[str] = [], raise_if_not_found: bool = True + ) -> Optional[dict]: """Read the record corresponding to the given `xml_id`.""" if '.' not in xml_id: raise ValueError('xml_id not valid') @@ -331,11 +331,13 @@ def execute(self, method: str, *args, **kw): def __repr__(self) -> str: return repr(self.odoo) + "/" + self.model - def fields(self, extended=False) -> Dict[str, dict]: + def fields(self, extended=False) -> dict[str, dict]: """Return the fields of the model""" if not self._field_info or (extended and not self._field_info['id'].get('name')): attributes = ( - None if extended else ['string', 'type', 'readonly', 'required', 'store', 'relation'] + None + if extended + else ['string', 'type', 'readonly', 'required', 'store', 'relation'] ) self._field_info = self.execute( 'fields_get', @@ -344,10 +346,10 @@ def fields(self, extended=False) -> Dict[str, dict]: ) return self._field_info # type: ignore - def __prepare_dict_fields(self, fields: Union[List[str], Dict[str, Dict]]) -> Dict[str, Dict]: + def __prepare_dict_fields(self, fields: Union[list[str], dict[str, dict]]) -> dict[str, dict]: """Make sure fields is a dict representing the data to get""" if isinstance(fields, list): - new_fields: Dict[str, Dict] = {} + new_fields: dict[str, dict] = {} for field in fields: level = new_fields for f in field.split('.'): @@ -484,11 +486,11 @@ def __read_dict_recursive(self, data, fields): return data - def _read(self, ids: List[int], fields: List[str], **kwargs): + def _read(self, ids: list[int], fields: list[str], **kwargs): """Raw read() function""" return self.read(ids, fields, load='raw', **kwargs) - def _search_read(self, domain: List, fields: List[str], **kwargs): + def _search_read(self, domain: list, fields: list[str], **kwargs): """Raw search_read() function""" if self.odoo.major_version >= 15: return self.search_read(domain, fields, load='raw', **kwargs) @@ -507,8 +509,8 @@ def _search_read(self, domain: List, fields: List[str], **kwargs): def read_dict( self, - ids: Union[List[int], int], - fields: Union[List[str], Dict[str, Dict]], + ids: Union[list[int], int], + fields: Union[list[str], dict[str, dict]], ): """Read with a dictionnary output and hierarchy view @@ -529,7 +531,7 @@ def read_dict( result = self.__read_dict_recursive(data, fields) return result[0] if single else result - def search_read_dict(self, domain: List, fields: Union[List[str], Dict[str, Dict]], **kwargs): + def search_read_dict(self, domain: list, fields: Union[list[str], dict[str, dict]], **kwargs): """Search read with a dictionnary output and hierarchy view Similar to `read_dict`. @@ -545,7 +547,7 @@ def search_read_dict(self, domain: List, fields: Union[List[str], Dict[str, Dict return self.__read_dict_recursive(data, fields) def read_group_dict( - self, domain: List, aggregates: Optional[List], groupby: List[str], **kwargs + self, domain: list, aggregates: Optional[list], groupby: list[str], **kwargs ): """Search read groupped data diff --git a/pyproject.toml b/pyproject.toml index 09646ba..6b64293 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,11 @@ dynamic = ["version"] description = "Simple RPC client for Odoo" readme = "README.md" keywords = ["odoo", "rpc"] -license = {text = "GNU Lesser General Public License v3 (LGPLv3)"} -requires-python = ">=3.7" +license = {file = "LICENSE"} +requires-python = ">=3.9" classifiers = [ # https://pypi.org/pypi?%3Aaction=list_classifiers - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Programming Language :: Python :: 3", "Framework :: Odoo", diff --git a/tests/mock_odoo_server.py b/tests/mock_odoo_server.py index 6cf3ad4..d40278a 100644 --- a/tests/mock_odoo_server.py +++ b/tests/mock_odoo_server.py @@ -27,7 +27,7 @@ def execute_kw( r = f(model, function, a, kw) if r is not None: return r - pytest.fail('execute_kw not implemented for %s.%s' % (model, function)) + pytest.fail(f'execute_kw not implemented for {model}.{function}') def generic(self, service: str, method: str, args: list): """Implement the call method from odoo""" @@ -37,7 +37,7 @@ def generic(self, service: str, method: str, args: list): r = f(service, method, args) if r is not None: return r - pytest.fail('%s.%s not implemented' % (service, method)) + pytest.fail(f'{service}.{method} not implemented') def patch_generic(self, f): """Append a method to `generic` call. @@ -116,7 +116,7 @@ def authenticate(service, method, args): return 1 if username and username == password: return 2 - raise OdooMockedError('Cannot authenticate on %s with %s' % (database, username)) + raise OdooMockedError(f'Cannot authenticate on {database} with {username}') @h.patch_execute_kw('res.users', 'fields_get') def field_get_user(allfields=[], attributes=[]):