diff --git a/CHANGELOG.md b/CHANGELOG.md index b039bf6..2b70590 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.0] - 2024-05-01 + +### Added + +- `get_combinations(values, length, start, stop, step, offset, indexes)`: Computes combinations given a set of values and a length. +- `minmax(*args)`: Returns with the min and the max value from the given arguments. +- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total. + +### Changed + +- Add options to JSONFile implementation (`sort_keys`, `skip_keys`, `ensure_ascii`, `separators`, `strict`). +- Set the default CSV dialect to `'excel'` when writing (this reflects the default value from the Python library). +- Set the default CSV dialect to `'auto'` when reading (the dialect will be sniffed from the first few rows). + +### Fixed + +- Fix the link to the documentation in the readme. +- Inconsistent return value in the log action. +- Too many branches and returns in the file checker. +- A few linter issues. +- Typo in the instructions for the installation in dev mode. + ## [0.9.1] - 2023-10-27 ### Fixed diff --git a/README.md b/README.md index 2fa7e6b..328d2ad 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ pip install --upgrade git+ssh://git@github.com/cerbernetix/py-toolbox.git@develo `py-toolbox` offers several utilities per domain. -Please refer to the [documentation](./docs/README.md) for more information. +Please refer to the [documentation](https://github.com/cerbernetix/py-toolbox/blob/main/docs/README.md) for more information. ## Development @@ -64,7 +64,7 @@ Then, create the virtual env and install the dependencies: ```sh cd py-toolbox python3 -m venv ".venv" -source "./venv/bin/activate" +source ".venv/bin/activate" pip install -r requirements.txt pip install -e . ``` diff --git a/docs/README.md b/docs/README.md index f42f347..a78b1a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ - [`toolbox.logging.log_file`](./toolbox.logging.log_file.md#module-toolboxlogginglog_file): A custom logger that writes directly to a file. - [`toolbox.math`](./toolbox.math.md#module-toolboxmath): A collection of Math related tools. - [`toolbox.math.combination`](./toolbox.math.combination.md#module-toolboxmathcombination): A set of functions for working with combinations. +- [`toolbox.math.utils`](./toolbox.math.utils.md#module-toolboxmathutils): A set of helper functions related to math. - [`toolbox.testing`](./toolbox.testing.md#module-toolboxtesting): The `testing` package provides utilities for testing purpose. - [`toolbox.testing.decorators`](./toolbox.testing.decorators.md#module-toolboxtestingdecorators): A collection of decorators for testing purpose. - [`toolbox.testing.test_case`](./toolbox.testing.test_case.md#module-toolboxtestingtest_case): Extends the default Python TestCase with more assertions. @@ -73,6 +74,9 @@ - [`config.setup_file_logging`](./toolbox.logging.config.md#function-setup_file_logging): Setup the application log to a file logger. - [`combination.get_combination_from_rank`](./toolbox.math.combination.md#function-get_combination_from_rank): Gets the combination corresponding to a particular rank. - [`combination.get_combination_rank`](./toolbox.math.combination.md#function-get_combination_rank): Gets the rank of a combination. +- [`combination.get_combinations`](./toolbox.math.combination.md#function-get_combinations): Yields lists of combined values according to the combinations defined by the lengths. +- [`utils.minmax`](./toolbox.math.utils.md#function-minmax): Returns with the min and the max value from the arguments. +- [`utils.quantity`](./toolbox.math.utils.md#function-quantity): Gets a quantity with respect to a quota applied to a total. - [`decorators.test_cases`](./toolbox.testing.decorators.md#function-test_cases): Creates a decorator for parametric test cases. diff --git a/docs/toolbox.files.csv_file.md b/docs/toolbox.files.csv_file.md index d239688..e827860 100644 --- a/docs/toolbox.files.csv_file.md +++ b/docs/toolbox.files.csv_file.md @@ -50,6 +50,7 @@ with file: --------------- - **CSV_ENCODING** - **CSV_DIALECT** +- **CSV_AUTO** - **CSV_SAMPLE_SIZE** - **CSV_READER_PARAMS** - **CSV_WRITER_PARAMS** @@ -57,7 +58,7 @@ with file: --- - + ## function `read_csv_file` @@ -65,7 +66,7 @@ with file: read_csv_file( filename: 'str', encoding: 'str' = 'utf-8', - dialect: 'str' = 'unix', + dialect: 'str' = 'auto', iterator: 'bool' = False, **kwargs ) → Iterable[dict | list] @@ -81,7 +82,7 @@ The returned value can be either a list (default) or an iterator (when the itera - `filename` (str): The path to the file to read. - `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING. - - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT. + - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO. - `iterator` (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False. - `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ','. - `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True. @@ -123,7 +124,7 @@ for row in read_csv_file('path/to/file', iterator=True): --- - + ## function `write_csv_file` @@ -132,7 +133,7 @@ write_csv_file( filename: 'str', data: 'Iterable[dict | list]', encoding: 'str' = 'utf-8', - dialect: 'str' = 'unix', + dialect: 'str' = 'excel', **kwargs ) → int ``` @@ -189,7 +190,7 @@ write_csv_file('path/to/file', csv_data, encoding='UTF-8', dialect='excel') --- - + ## function `read_zip_csv` @@ -199,7 +200,7 @@ read_zip_csv( filename: 'str' = None, encoding: 'str' = 'utf-8', decoding_errors: 'str' = 'ignore', - dialect: 'str' = 'unix', + dialect: 'str' = 'auto', iterator: 'bool' = False, **kwargs ) → Iterable[dict | list] @@ -217,7 +218,7 @@ The returned value can be either a list (default) or an iterator (when the itera - `filename` (str, optional): The name of the file to extract from the zip If omitted, the first file having a '.csv' extension will be selected. Defaults to None. - `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING. - `decoding_errors` (str, optional): Controls how decoding errors are handled. If 'strict', a UnicodeError exception is raised. Other possible values are 'ignore', 'replace', and any other name registered via codecs.register_error(). See Error Handlers for details. Defaults to "ignore". - - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT. + - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO. - `iterator` (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False. - `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ','. - `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True. @@ -265,7 +266,7 @@ with open('path/to/file.zip', 'rb') as file: --- - + ## class `CSVFile` Offers a simple API for reading and writing CSV files. @@ -310,7 +311,7 @@ with file(create=True): csv = file.read_file() ``` - + ### method `__init__` @@ -322,7 +323,7 @@ __init__( read: 'bool' = False, write: 'bool' = False, encoding: 'str' = 'utf-8', - dialect: 'str' = 'unix', + dialect: 'str' = 'auto', **kwargs ) ``` @@ -339,7 +340,7 @@ Creates a file manager for CSV files. - `read` (bool, optional): Expect to also read the file. Defaults to False. - `write` (bool, optional): Expect to also write to the file. Defaults to False. - `encoding` (str, optional): The file encoding. Defaults to CSV_ENCODING. - - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_DIALECT. + - `dialect` (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. Defaults to CSV_AUTO for reading or to CSV_DIALECT for writing. - `delimiter` (str, optional): A one-character string used to separate fields. Defaults to ",". - `doublequote` (bool, optional): Controls how instances of quotechar appearing inside a field should themselves be quoted. When True, the character is doubled. When False, the escapechar is used as a prefix to the quotechar. Defaults to True. - `escapechar` (str, optional): A one-character string used by the writer to escape the delimiter if quoting is set to QUOTE_NONE and the quotechar if doublequote is False. On reading, the escapechar removes any special meaning from the following character. Defaults to None, which disables escaping. @@ -566,7 +567,7 @@ size = file.size --- - + ### method `close` @@ -604,7 +605,7 @@ file.close() --- - + ### method `read` @@ -648,7 +649,7 @@ csv_data = [row for row in file] --- - + ### method `read_file` @@ -699,7 +700,7 @@ for row in file.read_file(iterator=True): --- - + ### method `write` @@ -746,7 +747,7 @@ with file(create=True): --- - + ### method `write_file` diff --git a/docs/toolbox.files.file.md b/docs/toolbox.files.file.md index 81c7cc5..c73856c 100644 --- a/docs/toolbox.files.file.md +++ b/docs/toolbox.files.file.md @@ -9,7 +9,9 @@ A collection of utilities for accessing files. **Examples:** ```python -from cerbernetix.toolbox.files import fetch_content, get_file_mode, read_file, read_zip_file, write_file +from cerbernetix.toolbox.files import ( + fetch_content, get_file_mode, read_file, read_zip_file, write_file +) # get_file_mode() is used to build a file access mode. # For example to create a text file: @@ -43,7 +45,7 @@ content = read_zip_file(data) --- - + ## function `get_file_mode` @@ -107,7 +109,7 @@ with open('path/to/file', get_file_mode(binary=True)) as file: --- - + ## function `read_file` @@ -159,7 +161,7 @@ data = read_file('path/to/file', binary=True) --- - + ## function `write_file` @@ -214,7 +216,7 @@ write_file('path/to/file', data, binary=True) --- - + ## function `fetch_content` @@ -240,7 +242,8 @@ Under the hood, it relies on requests to process the query. - `url` (str): The URL of the content to fetch. - `binary` (bool): Tells if the content is binary (True) or text (False). When True, the function will return a bytes sequence, otherwise it will return a string sequence. - `timeout` (int | tuple): The request timeout. Defaults to (6, 30). - - `**kwargs`: Additional parameters for the GET request. For more info, see [requests/api](https://requests.readthedocs.io/en/latest/api/). + - `**kwargs`: Additional parameters for the GET request. For more info, see + - `[requests/api](https`: //requests.readthedocs.io/en/latest/api/). @@ -277,7 +280,7 @@ data = fetch_content("http://example.com/data", binary=True) --- - + ## function `read_zip_file` diff --git a/docs/toolbox.files.file_manager.md b/docs/toolbox.files.file_manager.md index 15bbd3f..3af0f86 100644 --- a/docs/toolbox.files.file_manager.md +++ b/docs/toolbox.files.file_manager.md @@ -36,7 +36,7 @@ with file: --- - + ## class `FileManager` Offers a simple API for reading and writing files. @@ -74,7 +74,7 @@ with file(create=True): content = file.read_file() ``` - + ### method `__init__` @@ -293,7 +293,7 @@ size = file.size --- - + ### method `check` @@ -355,7 +355,7 @@ else: --- - + ### method `close` @@ -393,7 +393,7 @@ file.close() --- - + ### method `create_path` @@ -428,7 +428,7 @@ else: --- - + ### method `delete` @@ -467,7 +467,7 @@ file.delete() --- - + ### method `exists` @@ -499,7 +499,7 @@ if file.exists(): --- - + ### method `open` @@ -560,7 +560,7 @@ data = [dat for dat in file] --- - + ### method `read` @@ -601,7 +601,7 @@ with file: --- - + ### method `read_file` @@ -650,7 +650,7 @@ for data in file.read_file(iterator=True): --- - + ### method `write` @@ -696,7 +696,7 @@ with file(create=True): --- - + ### method `write_file` diff --git a/docs/toolbox.files.json_file.md b/docs/toolbox.files.json_file.md index 7f75082..8d1e219 100644 --- a/docs/toolbox.files.json_file.md +++ b/docs/toolbox.files.json_file.md @@ -46,15 +46,25 @@ with file: --------------- - **JSON_ENCODING** - **JSON_INDENT** +- **JSON_SEPARATORS** +- **JSON_SORT_KEYS** +- **JSON_SKIP_KEYS** +- **JSON_ENSURE_ASCII** +- **JSON_STRICT** --- - + ## function `read_json_file` ```python -read_json_file(filename: str, encoding: str = 'utf-8', **kwargs) → Any +read_json_file( + filename: str, + encoding: str = 'utf-8', + strict: bool = True, + **kwargs +) → Any ``` Reads a JSON content from a file. @@ -65,6 +75,7 @@ Reads a JSON content from a file. - `filename` (str): The path to the file to read. - `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING. + - `strict` (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT. @@ -91,7 +102,7 @@ json_data = read_json_file('path/to/file', encoding='UTF-8') --- - + ## function `write_json_file` @@ -101,6 +112,10 @@ write_json_file( data: Any, encoding: str = 'utf-8', indent: int = 4, + separators: tuple = None, + sort_keys: bool = False, + skip_keys: bool = False, + ensure_ascii: bool = True, **kwargs ) → int ``` @@ -115,6 +130,10 @@ Writes a JSON content to a file. - `data` (Any): The content to write to the file. - `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING. - `indent` (int, optional): The line indent. Defaults to JSON_INDENT. + - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. Defaults to JSON_SEPARATORS. + - `sort_keys` (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS. + - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type. Defaults to JSON_SKIP_KEYS. + - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars. Defaults to JSON_ENSURE_ASCII. @@ -146,7 +165,7 @@ write_json_file('path/to/file', json_data, encoding='UTF-8', indent=2) --- - + ## class `JSONFile` Offers a simple API for reading and writing JSON files. @@ -163,6 +182,11 @@ The read API reads all the content at once, and so do the write API too. - `binary` (bool): The type of file, say text. It must always be False. - `encoding` (str, optional): The file encoding. - `indent` (int, optional): The line indent. + - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. + - `sort_keys` (bool, optional): Whether or not to sort the keys. + - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type. + - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars. + - `strict` (bool, optional): Whether or not to forbid control chars. @@ -187,7 +211,7 @@ with file(create=True): json = file.read_file() ``` - + ### method `__init__` @@ -200,6 +224,11 @@ __init__( write: bool = False, encoding: str = 'utf-8', indent: int = 4, + separators: tuple = None, + sort_keys: bool = False, + skip_keys: bool = False, + ensure_ascii: bool = True, + strict: bool = True, **kwargs ) ``` @@ -217,6 +246,11 @@ Creates a file manager for JSON files. - `write` (bool, optional): Expect to also write to the file. Defaults to False. - `encoding` (str, optional): The file encoding. Defaults to JSON_ENCODING. - `indent` (int, optional): The line indent. Defaults to JSON_INDENT. + - `separators` (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. Defaults to JSON_SEPARATORS. + - `sort_keys` (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS. + - `skip_keys` (bool, optional): Whether or not to skip the keys not having an allowed type. Defaults to JSON_SKIP_KEYS. + - `ensure_ascii` (bool, optional): Whether or not to escape non-ascii chars. Defaults to JSON_ENSURE_ASCII. + - `strict` (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT. @@ -424,7 +458,7 @@ size = file.size --- - + ### method `read` @@ -464,7 +498,7 @@ with file: --- - + ### method `write` diff --git a/docs/toolbox.files.md b/docs/toolbox.files.md index dce1f8f..a26f814 100644 --- a/docs/toolbox.files.md +++ b/docs/toolbox.files.md @@ -73,10 +73,16 @@ csv_data = file.read_zip_csv(data) **Global Variables** --------------- +- **CSV_AUTO** - **CSV_DIALECT** - **CSV_ENCODING** - **JSON_ENCODING** +- **JSON_ENSURE_ASCII** - **JSON_INDENT** +- **JSON_SEPARATORS** +- **JSON_SKIP_KEYS** +- **JSON_SORT_KEYS** +- **JSON_STRICT** diff --git a/docs/toolbox.files.pickle_file.md b/docs/toolbox.files.pickle_file.md index 1cb4dc9..e8888eb 100644 --- a/docs/toolbox.files.pickle_file.md +++ b/docs/toolbox.files.pickle_file.md @@ -54,7 +54,7 @@ with file: --- - + ## function `read_pickle_file` @@ -106,7 +106,7 @@ for obj in read_pickle_file('path/to/file', iterator=True): --- - + ## function `write_pickle_file` @@ -156,7 +156,7 @@ write_pickle_file('path/to/file', data) --- - + ## class `PickleFile` Offers a simple API for reading and writing pickle files. @@ -200,7 +200,7 @@ file.write_file(data) data = file.read_file() ``` - + ### method `__init__` @@ -439,7 +439,7 @@ size = file.size --- - + ### method `read` @@ -483,7 +483,7 @@ data = [obj for obj in file] --- - + ### method `read_file` @@ -534,7 +534,7 @@ for obj in file.read_file(iterator=True): --- - + ### method `write` @@ -581,7 +581,7 @@ with file(create=True): --- - + ### method `write_file` diff --git a/docs/toolbox.logging.log_file.md b/docs/toolbox.logging.log_file.md index 48e1bc5..e0b51a1 100644 --- a/docs/toolbox.logging.log_file.md +++ b/docs/toolbox.logging.log_file.md @@ -40,7 +40,7 @@ logger.error('An error occurred: %s', error) --- - + ## class `LogFile` Offers a similar API to the Python builtin loggers for logging to a custom file. @@ -68,7 +68,7 @@ logger = LogFile() logger.info('The received value is %d', value) ``` - + ### method `__init__` @@ -233,7 +233,7 @@ print(logger.name) --- - + ### method `close` @@ -266,7 +266,7 @@ logger.close() # not necessary since it will be called automatically upon exit. --- - + ### method `debug` @@ -306,7 +306,7 @@ logger.debug('For debug purpose: %d given to %s', value, action) --- - + ### method `delete` @@ -335,7 +335,7 @@ logger.delete() --- - + ### method `error` @@ -375,7 +375,7 @@ logger.error('An error occurred: %s', error) --- - + ### method `info` @@ -415,7 +415,7 @@ logger.info('The received value is %d', value) --- - + ### method `log` @@ -459,7 +459,7 @@ logger.log(logging.ERROR, 'An error occurred: %s', error) --- - + ### method `open` @@ -492,7 +492,7 @@ logger.info('Something was done at %s', datetime.now()) --- - + ### method `set_format` @@ -527,7 +527,7 @@ logger.set_format('[%(asctime)s][%(levelname)s]: %(message)s') --- - + ### method `set_level` @@ -562,7 +562,7 @@ logger.set_level(logging.DEBUG) --- - + ### method `warn` diff --git a/docs/toolbox.math.combination.md b/docs/toolbox.math.combination.md index 04592f7..cf9e851 100644 --- a/docs/toolbox.math.combination.md +++ b/docs/toolbox.math.combination.md @@ -9,19 +9,31 @@ A set of functions for working with combinations. **Examples:** ```python -from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank +from cerbernetix.toolbox.math import ( + get_combination_rank, + get_combination_from_rank, + get_combinations, +) # Get the rank of a combination of 3 numbers print(get_combination_rank([1, 3, 5])) # Get the combination of 3 numbers ranked at position 5 -print(list(get_combination_from_rank(5, 3))) +print(get_combination_from_rank(5, 3)) + +# Get the combinations of 3 numbers from the list +values = [1, 2, 4, 8, 16] +print(list(get_combinations(values, 3))) + +# Get the combinations of 3 numbers out of 50 from rank 200 to 500 +values = [1, 2, 4, 8, 16] +print(list(get_combinations(50, 3, start=200, stop=300))) ``` --- - + ## function `get_combination_rank` @@ -67,7 +79,7 @@ print(get_combination_rank([1, 3, 5])) --- - + ## function `get_combination_from_rank` @@ -111,7 +123,93 @@ The rank must start at 0. from cerbernetix.toolbox.math import get_combination_from_rank # Get the combination of 3 numbers ranked at position 5 -print(list(get_combination_from_rank(5, 3))) +print(get_combination_from_rank(5, 3)) +``` + + +--- + + + +## function `get_combinations` + +```python +get_combinations( + values: int | list | tuple | dict, + length: int = 2, + start: int = 0, + stop: int = None, + step: int = 1, + offset: int = 0, + indexes: list | tuple = None +) → Iterator[list] +``` + +Yields lists of combined values according to the combinations defined by the lengths. + +Taking a list of values and the length of a combination, it yields each combination of length elements taken from the values. + +Note: Beware, the number of possible combinations grows fast with the lengths. For example, 3 out of 5 gives 10 possible combinations, but 3 out of 50 gives 19600... + + + +**Args:** + + - `values` (int | list | tuple | dict): The list of values from which build the list of combinations. It can be either the length of a range of integers from 0, or a list of sparse values. + - `length` (int, optional): The length of each combination. Defaults to 2. + - `start` (int, optional): The rank of the first combination to generate. Defaults to 0. + - `stop` (int, optional): The rank of the last combination before what stop the generation. If omitted, the maximum number of combination is taken. Defaults to None. + - `step` (int, optional): The step between ranks. If start is higher than stop, the step is set to a negative value. Defaults to 1. + - `offset` (int, optional): An offset to add to the values if they must not start at 0. Defaults to 0. + - `indexes` (list | tuple, optional): A list of indexes for retrieving the values by position. Useful if the values are not indexed by sequential numbers or with a contiguous set like a dictionary or a spare array. Defaults to None. + + + +**Yields:** + + - `Iterator[list]`: A list of combined values by the given length. + + + +**Examples:** + ```python +from cerbernetix.toolbox.math import get_combinations + +# Get the combinations of 3 numbers from the list +values = [1, 2, 4, 8, 16] +print(list(get_combinations(values, 3))) +# [[1, 2, 4], +# [1, 2, 8], +# [1, 4, 8], +# [2, 4, 8], +# [1, 2, 16], +# [1, 4, 16], +# [2, 4, 16], +# [1, 8, 16], +# [2, 8, 16], +# [4, 8, 16]] + +# Get the combinations of 3 numbers from the list from rank 4 to 8 +values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16} +indexes = ["1", "2", "4", "8", "16"] +print(list(get_combinations(values, 3, indexes=indexes, start=4, stop=8))) +# [[1, 2, 16], +# [1, 4, 16], +# [2, 4, 16], +# [1, 8, 16]] + +# Get combinations from a number of values +print(list(get_combinations(5, 3, offset=1))) +# [[1, 2, 3], +# [1, 2, 4], +# [1, 3, 4], +# [2, 3, 4], +# [1, 2, 5], +# [1, 3, 5], +# [2, 3, 5], +# [1, 4, 5], +# [2, 4, 5], +# [3, 4, 5]] ``` diff --git a/docs/toolbox.math.md b/docs/toolbox.math.md index 6335ba9..0f6dae9 100644 --- a/docs/toolbox.math.md +++ b/docs/toolbox.math.md @@ -8,18 +8,49 @@ A collection of Math related tools. It contains: - `get_combination_rank(combination, offset)`: Gets the rank of a combination. - `get_combination_from_rank(rank, length, offset)`: Gets the combination corresponding to a particular rank. +- `get_combinations(values, length, start, stop, step, offset, indexes)`: Yields lists of combined values according to the combinations defined by the lengths. +- `minmax(*args)`: Returns with the min and the max value from the given arguments. +- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total. **Examples:** ```python -from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank +from cerbernetix.toolbox.math import ( + get_combination_rank, + get_combination_from_rank, + get_combinations, +) # Get the rank of a combination of 3 numbers print(get_combination_rank([1, 3, 5])) # Get the combination of 3 numbers ranked at position 5 -print(list(get_combination_from_rank(5, 3))) +print(get_combination_from_rank(5, 3)) + +# Get the combinations of 3 numbers from the list +values = [1, 2, 4, 8, 16] +print(list(get_combinations(values, 3))) + +# Get the combinations of 3 numbers out of 50 from rank 200 to 500 +values = [1, 2, 4, 8, 16] +print(list(get_combinations(50, 3, start=200, stop=300))) +``` + +```python +from cerbernetix.toolbox.math import minmax + +mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 +``` + +```python +from cerbernetix.toolbox.math import quantity + +# Gets a size from a percentage +size = quantity(.2, 10) # 2 + +# Gets a size from an absolute value +size = quantity(6, 10) # 6 ``` diff --git a/docs/toolbox.math.utils.md b/docs/toolbox.math.utils.md new file mode 100644 index 0000000..adf0bf9 --- /dev/null +++ b/docs/toolbox.math.utils.md @@ -0,0 +1,96 @@ + + + + +# module `toolbox.math.utils` +A set of helper functions related to math. + +Examples ```python +from cerbernetix.toolbox.math import minmax + +mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 +``` + +```python +from cerbernetix.toolbox.math import quantity + +# Gets a size from a percentage +size = quantity(.2, 10) # 2 + +# Gets a size from an absolute value +size = quantity(6, 10) # 6 +``` + + +--- + + + +## function `minmax` + +```python +minmax(*args) → tuple +``` + +Returns with the min and the max value from the arguments. + + + +**Args:** + + - `*args`: Arguments from which extract the min and the max. + + + +**Returns:** + + - `tuple`: A tuple with first the min value, then the max value. + +Examples ```python +from cerbernetix.toolbox.math import minmax + +mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 +``` + + +--- + + + +## function `quantity` + +```python +quantity(quota: int | float, total: int) → int +``` + +Gets a quantity with respect to a quota applied to a total. + + + +**Args:** + + - `quota` (int | float): The expected quota from the total. It can be either a percentage or an absolute value. The percentage is represented by a number between 0 and 1. An absolute value is represented by a number between 1 and the total included. + - `total` (int): The total number. + + + +**Returns:** + + - `int`: The quantity computed from the quota applied to the total. It cannot exceeds the total, and it cannot be negative. + +Examples ```python +from cerbernetix.toolbox.math import quantity + +# Gets a size from a percentage +size = quantity(.2, 10) # 2 + +# Gets a size from an absolute value +size = quantity(6, 10) # 6 +``` + + + + +--- + +_This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ diff --git a/docs/toolbox.testing.test_case.md b/docs/toolbox.testing.test_case.md index 0892e5d..b903f88 100644 --- a/docs/toolbox.testing.test_case.md +++ b/docs/toolbox.testing.test_case.md @@ -23,7 +23,7 @@ class TestMyStuff(testing.TestCase) --- - + ## class `TestCase` Test class with additional assertions. @@ -33,7 +33,7 @@ Test class with additional assertions. --- - + ### method `assertListsAlmostEqual` @@ -42,7 +42,7 @@ assertListsAlmostEqual( first: Iterable[float], second: Iterable[float], places: int = 7 -) +) → None ``` Asserts that 2 lists of float numbers are almost equal by the number of places. @@ -74,7 +74,7 @@ class TestMyStuff(testing.TestCase) --- - + ### method `assertListsNotAlmostEqual` @@ -83,7 +83,7 @@ assertListsNotAlmostEqual( first: Iterable[float], second: Iterable[float], places: int = 7 -) +) → None ``` Asserts that 2 lists of float numbers are not almost equal by the number of places. diff --git a/pyproject.toml b/pyproject.toml index 7d157fc..4d988bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cerbernetix.toolbox" -version = "0.9.1" +version = "0.10.0" authors = [{ name = "Jean-Sébastien CONAN", email = "jsconan@gmail.com" }] description = "A set of utilities for Python projects" readme = "README.md" diff --git a/src/cerbernetix/toolbox/files/__init__.py b/src/cerbernetix/toolbox/files/__init__.py index 7c84fdf..e98bc30 100644 --- a/src/cerbernetix/toolbox/files/__init__.py +++ b/src/cerbernetix/toolbox/files/__init__.py @@ -64,7 +64,9 @@ csv_data = file.read_zip_csv(data) ``` """ + from cerbernetix.toolbox.files.csv_file import ( + CSV_AUTO, CSV_DIALECT, CSV_ENCODING, CSVFile, @@ -82,7 +84,12 @@ from cerbernetix.toolbox.files.file_manager import FileManager from cerbernetix.toolbox.files.json_file import ( JSON_ENCODING, + JSON_ENSURE_ASCII, JSON_INDENT, + JSON_SEPARATORS, + JSON_SKIP_KEYS, + JSON_SORT_KEYS, + JSON_STRICT, JSONFile, read_json_file, write_json_file, diff --git a/src/cerbernetix/toolbox/files/csv_file.py b/src/cerbernetix/toolbox/files/csv_file.py index e1ad18e..3adb4d4 100644 --- a/src/cerbernetix/toolbox/files/csv_file.py +++ b/src/cerbernetix/toolbox/files/csv_file.py @@ -39,6 +39,7 @@ first = file.read() ``` """ + from __future__ import annotations import csv @@ -52,7 +53,10 @@ CSV_ENCODING = "utf-8" # The default CSV dialect -CSV_DIALECT = "unix" +CSV_DIALECT = "excel" + +# The value for auto-detecting the CSV dialect +CSV_AUTO = "auto" # The amount of bytes to read for auto-detecting the CSV dialect CSV_SAMPLE_SIZE = 1024 @@ -139,7 +143,7 @@ def __init__( read: bool = False, write: bool = False, encoding: str = CSV_ENCODING, - dialect: str = CSV_DIALECT, + dialect: str = CSV_AUTO, **kwargs, ): r"""Creates a file manager for CSV files. @@ -157,7 +161,7 @@ def __init__( encoding (str, optional): The file encoding. Defaults to CSV_ENCODING. dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. - Defaults to CSV_DIALECT. + Defaults to CSV_AUTO for reading or to CSV_DIALECT for writing. delimiter (str, optional): A one-character string used to separate fields. Defaults to ",". doublequote (bool, optional): Controls how instances of quotechar appearing inside a @@ -400,7 +404,7 @@ def read(self) -> dict | list: reader = csv.DictReader dialect = self.dialect - if dialect == "auto": + if dialect == CSV_AUTO: dialect = csv.Sniffer().sniff(self._file.read(CSV_SAMPLE_SIZE)) self._file.seek(0) @@ -461,7 +465,7 @@ def write(self, data: dict | list) -> int: writer = csv.writer dialect = self.dialect - if dialect == "auto": + if dialect == CSV_AUTO: dialect = CSV_DIALECT self._writer = writer(self._file, dialect=dialect, **kwargs) @@ -475,7 +479,7 @@ def write(self, data: dict | list) -> int: def read_csv_file( filename: str, encoding: str = CSV_ENCODING, - dialect: str = CSV_DIALECT, + dialect: str = CSV_AUTO, iterator: bool = False, **kwargs, ) -> Iterable[dict | list]: @@ -489,7 +493,7 @@ def read_csv_file( encoding (str, optional): The file encoding. Defaults to CSV_ENCODING. dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. - Defaults to CSV_DIALECT. + Defaults to CSV_AUTO. iterator (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False. delimiter (str, optional): A one-character string used to separate fields. @@ -622,7 +626,7 @@ def read_zip_csv( filename: str = None, encoding: str = CSV_ENCODING, decoding_errors: str = "ignore", - dialect: str = CSV_DIALECT, + dialect: str = CSV_AUTO, iterator: bool = False, **kwargs, ) -> Iterable[dict | list]: @@ -642,7 +646,7 @@ def read_zip_csv( Defaults to "ignore". dialect (str, optional): The CSV dialect to use. If 'auto' is given, the reader will try detecting the CSV dialect by reading a sample at the head of the file. - Defaults to CSV_DIALECT. + Defaults to CSV_AUTO. iterator (bool, optional): When True, the function will return an iterator instead of a list. Defaults to False. delimiter (str, optional): A one-character string used to separate fields. @@ -704,7 +708,7 @@ def read_zip_csv( else: reader_factory = csv.DictReader - if dialect == "auto": + if dialect == CSV_AUTO: dialect = csv.Sniffer().sniff(text[:CSV_SAMPLE_SIZE]) lines = re.split(r"[\r\n]+", text.strip("\r\n")) diff --git a/src/cerbernetix/toolbox/files/file.py b/src/cerbernetix/toolbox/files/file.py index 914d83a..5d064ae 100644 --- a/src/cerbernetix/toolbox/files/file.py +++ b/src/cerbernetix/toolbox/files/file.py @@ -2,7 +2,9 @@ Examples: ```python -from cerbernetix.toolbox.files import fetch_content, get_file_mode, read_file, read_zip_file, write_file +from cerbernetix.toolbox.files import ( + fetch_content, get_file_mode, read_file, read_zip_file, write_file +) # get_file_mode() is used to build a file access mode. # For example to create a text file: @@ -33,6 +35,7 @@ content = read_zip_file(data) ``` """ + import os import zipfile from io import BytesIO @@ -222,7 +225,8 @@ def fetch_content( binary (bool): Tells if the content is binary (True) or text (False). When True, the function will return a bytes sequence, otherwise it will return a string sequence. timeout (int | tuple): The request timeout. Defaults to (6, 30). - **kwargs: Additional parameters for the GET request. For more info, see [requests/api](https://requests.readthedocs.io/en/latest/api/). + **kwargs: Additional parameters for the GET request. For more info, see + [requests/api](https://requests.readthedocs.io/en/latest/api/). Raises: requests.RequestException: There was an ambiguous exception that occurred while handling diff --git a/src/cerbernetix/toolbox/files/file_manager.py b/src/cerbernetix/toolbox/files/file_manager.py index 5241064..dd4e711 100644 --- a/src/cerbernetix/toolbox/files/file_manager.py +++ b/src/cerbernetix/toolbox/files/file_manager.py @@ -25,6 +25,7 @@ print(file.read()) ``` """ + from __future__ import annotations import os @@ -319,6 +320,7 @@ def open( """ self.close() + # pylint: disable-next=consider-using-with self._file = open( self.filename, mode=get_file_mode(create, append, read, write, self.binary), @@ -568,41 +570,26 @@ def check( if must_exist and not exist: return False - if min_time is not None or max_time is not None: - if not exist: - return False - - file_time = self.date - - if min_time is not None and file_time <= min_time: - return False - - if max_time is not None and file_time >= max_time: - return False - - if min_age is not None or max_age is not None: - if not exist: - return False - - file_age = self.age + def fail(value: int, value_min: int, value_max: int) -> bool: + if value_min is not None or value_max is not None: + if not exist: + return True - if min_age is not None and file_age <= min_age: - return False + if value_min is not None and value <= value_min: + return True - if max_age is not None and file_age >= max_age: - return False - - if min_size is not None or max_size is not None: - if not exist: - return False + if value_max is not None and value >= value_max: + return True + return False - file_size = self.size + if fail(self.date, min_time, max_time): + return False - if min_size is not None and file_size <= min_size: - return False + if fail(self.age, min_age, max_age): + return False - if max_size is not None and file_size >= max_size: - return False + if fail(self.size, min_size, max_size): + return False return True diff --git a/src/cerbernetix/toolbox/files/json_file.py b/src/cerbernetix/toolbox/files/json_file.py index 58aefb2..06ea5b7 100644 --- a/src/cerbernetix/toolbox/files/json_file.py +++ b/src/cerbernetix/toolbox/files/json_file.py @@ -35,6 +35,7 @@ json_data = file.read() ``` """ + import json from typing import Any @@ -46,6 +47,21 @@ # The default indent for JSON files JSON_INDENT = 4 +# The default separators for JSON files +JSON_SEPARATORS = None + +# The default value for whether or not to sort the keys in JSON files +JSON_SORT_KEYS = False + +# The default value for whether or not to skip the keys not having an allowed type in JSON files +JSON_SKIP_KEYS = False + +# The default value for escaping non-ascii chars in JSON files +JSON_ENSURE_ASCII = True + +# The default value for forbidding the control chars in JSON files +JSON_STRICT = True + class JSONFile(FileManager): """Offers a simple API for reading and writing JSON files. @@ -60,6 +76,11 @@ class JSONFile(FileManager): binary (bool): The type of file, say text. It must always be False. encoding (str, optional): The file encoding. indent (int, optional): The line indent. + separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. + sort_keys (bool, optional): Whether or not to sort the keys. + skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type. + ensure_ascii (bool, optional): Whether or not to escape non-ascii chars. + strict (bool, optional): Whether or not to forbid control chars. Examples: ```python @@ -92,6 +113,11 @@ def __init__( write: bool = False, encoding: str = JSON_ENCODING, indent: int = JSON_INDENT, + separators: tuple = JSON_SEPARATORS, + sort_keys: bool = JSON_SORT_KEYS, + skip_keys: bool = JSON_SKIP_KEYS, + ensure_ascii: bool = JSON_ENSURE_ASCII, + strict: bool = JSON_STRICT, **kwargs, ): """Creates a file manager for JSON files. @@ -108,6 +134,15 @@ def __init__( Defaults to False. encoding (str, optional): The file encoding. Defaults to JSON_ENCODING. indent (int, optional): The line indent. Defaults to JSON_INDENT. + separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. + Defaults to JSON_SEPARATORS. + sort_keys (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS. + skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type. + Defaults to JSON_SKIP_KEYS. + ensure_ascii (bool, optional): Whether or not to escape non-ascii chars. + Defaults to JSON_ENSURE_ASCII. + strict (bool, optional): Whether or not to forbid control chars. + Defaults to JSON_STRICT. Examples: ```python @@ -153,6 +188,11 @@ def __init__( **kwargs, ) self.indent = indent + self.separators = separators + self.sort_keys = sort_keys + self.skip_keys = skip_keys + self.ensure_ascii = ensure_ascii + self.strict = strict def read(self) -> Any: """Reads the content from the file. @@ -182,7 +222,7 @@ def read(self) -> Any: if not data: return None - return json.JSONDecoder().decode(data) + return json.JSONDecoder(strict=self.strict).decode(data) def write(self, data: Any) -> int: """Writes content to the file. @@ -212,8 +252,11 @@ def write(self, data: Any) -> int: """ return super().write( json.JSONEncoder( - sort_keys=True, + skipkeys=self.skip_keys, + ensure_ascii=self.ensure_ascii, + sort_keys=self.sort_keys, indent=self.indent, + separators=self.separators, ).encode(data) ) @@ -221,6 +264,7 @@ def write(self, data: Any) -> int: def read_json_file( filename: str, encoding: str = JSON_ENCODING, + strict: bool = JSON_STRICT, **kwargs, ) -> Any: """Reads a JSON content from a file. @@ -228,6 +272,7 @@ def read_json_file( Args: filename (str): The path to the file to read. encoding (str, optional): The file encoding. Defaults to JSON_ENCODING. + strict (bool, optional): Whether or not to forbid control chars. Defaults to JSON_STRICT. Raises: OSError: If the file cannot be read. @@ -243,7 +288,7 @@ def read_json_file( json_data = read_json_file('path/to/file', encoding='UTF-8') ``` """ - return JSONFile(filename, encoding=encoding, **kwargs).read_file() + return JSONFile(filename, encoding=encoding, strict=strict, **kwargs).read_file() def write_json_file( @@ -251,6 +296,10 @@ def write_json_file( data: Any, encoding: str = JSON_ENCODING, indent: int = JSON_INDENT, + separators: tuple = JSON_SEPARATORS, + sort_keys: bool = JSON_SORT_KEYS, + skip_keys: bool = JSON_SKIP_KEYS, + ensure_ascii: bool = JSON_ENSURE_ASCII, **kwargs, ) -> int: """Writes a JSON content to a file. @@ -260,6 +309,13 @@ def write_json_file( data (Any): The content to write to the file. encoding (str, optional): The file encoding. Defaults to JSON_ENCODING. indent (int, optional): The line indent. Defaults to JSON_INDENT. + separators (tuple, optional): The separators for key/values, a.k.a `(', ', ': ')`. + Defaults to JSON_SEPARATORS. + sort_keys (bool, optional): Whether or not to sort the keys. Defaults to JSON_SORT_KEYS. + skip_keys (bool, optional): Whether or not to skip the keys not having an allowed type. + Defaults to JSON_SKIP_KEYS. + ensure_ascii (bool, optional): Whether or not to escape non-ascii chars. + Defaults to JSON_ENSURE_ASCII. Raises: OSError: If the file cannot be written. @@ -284,5 +340,9 @@ def write_json_file( filename, encoding=encoding, indent=indent, + separators=separators, + sort_keys=sort_keys, + skip_keys=skip_keys, + ensure_ascii=ensure_ascii, **kwargs, ).write_file(data) diff --git a/src/cerbernetix/toolbox/files/pickle_file.py b/src/cerbernetix/toolbox/files/pickle_file.py index 50ba8e2..ba5b63c 100644 --- a/src/cerbernetix/toolbox/files/pickle_file.py +++ b/src/cerbernetix/toolbox/files/pickle_file.py @@ -39,6 +39,7 @@ first = file.read() ``` """ + from __future__ import annotations import pickle @@ -134,17 +135,17 @@ def __init__( pickle will try to map the new Python 3 names to the old module names used in Python 2, so that the pickle data stream is readable with Python 2. Defaults to True. encoding (str, optional): Tell pickle how to decode 8-bit string instances pickled by - Python 2. The encoding can be ‘bytes’ to read these 8-bit string instances as bytes objects. - Using encoding='latin1' is required for unpickling NumPy arrays and instances of datetime, - date and time pickled by Python 2. Defaults to ‘ASCII’. + Python 2. The encoding can be ‘bytes’ to read these 8-bit string instances as bytes + objects. Using encoding='latin1' is required for unpickling NumPy arrays and instances + of datetime, date and time pickled by Python 2. Defaults to ‘ASCII’. errors (str, optional): Tell pickle how to decode 8-bit string instances pickled by Python 2. Defaults to ‘strict’. buffers (optional): If buffers is None (the default), then all data necessary for - deserialization must be contained in the pickle stream. This means that the buffer_callback - argument was None when a Pickler was instantiated (or when dump() or dumps() was called). If - buffers is not None, it should be an iterable of buffer-enabled objects that is consumed - each time the pickle stream references an out-of-band buffer view. Such buffers have been - given in order to the buffer_callback of a Pickler object. + deserialization must be contained in the pickle stream. This means that the + buffer_callback argument was None when a Pickler was instantiated (or when dump() or + dumps() was called). If buffers is not None, it should be an iterable of buffer-enabled + objects that is consumed each time the pickle stream references an out-of-band buffer + view. Such buffers have been given in order to the buffer_callback of a Pickler object. buffer_callback (optional): If buffer_callback is None (the default), buffer views are serialized into file as part of the pickle stream. If buffer_callback is not None, then it can be called any number of times with a buffer view. If the callback returns a false diff --git a/src/cerbernetix/toolbox/logging/log_file.py b/src/cerbernetix/toolbox/logging/log_file.py index aab28dd..720241f 100644 --- a/src/cerbernetix/toolbox/logging/log_file.py +++ b/src/cerbernetix/toolbox/logging/log_file.py @@ -23,6 +23,7 @@ logger.error('An error occurred: %s', error) ``` """ + from __future__ import annotations import atexit @@ -290,7 +291,7 @@ def log(self, level: int, message: str, *args, **kwargs) -> LogFile: ``` """ if level < self._level: - return + return self pathname = None lineno = 0 diff --git a/src/cerbernetix/toolbox/math/__init__.py b/src/cerbernetix/toolbox/math/__init__.py index c44772d..47e0285 100644 --- a/src/cerbernetix/toolbox/math/__init__.py +++ b/src/cerbernetix/toolbox/math/__init__.py @@ -4,16 +4,54 @@ - `get_combination_rank(combination, offset)`: Gets the rank of a combination. - `get_combination_from_rank(rank, length, offset)`: Gets the combination corresponding to a particular rank. +- `get_combinations(values, length, start, stop, step, offset, indexes)`: Yields lists of combined +values according to the combinations defined by the lengths. +- `minmax(*args)`: Returns with the min and the max value from the given arguments. +- `quantity(quota, total)`: Gets a quantity with respect to a quota applied to a total. Examples: ```python -from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank +from cerbernetix.toolbox.math import ( + get_combination_rank, + get_combination_from_rank, + get_combinations, +) # Get the rank of a combination of 3 numbers print(get_combination_rank([1, 3, 5])) # Get the combination of 3 numbers ranked at position 5 -print(list(get_combination_from_rank(5, 3))) +print(get_combination_from_rank(5, 3)) + +# Get the combinations of 3 numbers from the list +values = [1, 2, 4, 8, 16] +print(list(get_combinations(values, 3))) + +# Get the combinations of 3 numbers out of 50 from rank 200 to 500 +values = [1, 2, 4, 8, 16] +print(list(get_combinations(50, 3, start=200, stop=300))) +``` + +```python +from cerbernetix.toolbox.math import minmax + +mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 +``` + +```python +from cerbernetix.toolbox.math import quantity + +# Gets a size from a percentage +size = quantity(.2, 10) # 2 + +# Gets a size from an absolute value +size = quantity(6, 10) # 6 ``` """ -from cerbernetix.toolbox.math.combination import get_combination_from_rank, get_combination_rank + +from cerbernetix.toolbox.math.combination import ( + get_combination_from_rank, + get_combination_rank, + get_combinations, +) +from cerbernetix.toolbox.math.utils import minmax, quantity diff --git a/src/cerbernetix/toolbox/math/combination.py b/src/cerbernetix/toolbox/math/combination.py index 2dfc304..3baaa2e 100644 --- a/src/cerbernetix/toolbox/math/combination.py +++ b/src/cerbernetix/toolbox/math/combination.py @@ -2,17 +2,32 @@ Examples: ```python -from cerbernetix.toolbox.math import get_combination_rank, get_combination_from_rank +from cerbernetix.toolbox.math import ( + get_combination_rank, + get_combination_from_rank, + get_combinations, +) # Get the rank of a combination of 3 numbers print(get_combination_rank([1, 3, 5])) # Get the combination of 3 numbers ranked at position 5 -print(list(get_combination_from_rank(5, 3))) +print(get_combination_from_rank(5, 3)) + +# Get the combinations of 3 numbers from the list +values = [1, 2, 4, 8, 16] +print(list(get_combinations(values, 3))) + +# Get the combinations of 3 numbers out of 50 from rank 200 to 500 +values = [1, 2, 4, 8, 16] +print(list(get_combinations(50, 3, start=200, stop=300))) ``` """ + from math import comb -from typing import Iterable +from typing import Iterable, Iterator + +from cerbernetix.toolbox.data.mappers import passthrough def get_combination_rank(combination: Iterable[int], offset: int = 0) -> int: @@ -77,7 +92,7 @@ def get_combination_from_rank(rank: int, length: int = 2, offset: int = 0) -> li from cerbernetix.toolbox.math import get_combination_from_rank # Get the combination of 3 numbers ranked at position 5 - print(list(get_combination_from_rank(5, 3))) + print(get_combination_from_rank(5, 3)) ``` """ if rank < 0: @@ -115,3 +130,116 @@ def get_combination_from_rank(rank: int, length: int = 2, offset: int = 0) -> li combination[0] = rank - binomial + offset return combination + + +def get_combinations( + values: int | list | tuple | dict, + length: int = 2, + start: int = 0, + stop: int = None, + step: int = 1, + offset: int = 0, + indexes: list | tuple = None, +) -> Iterator[list]: + """Yields lists of combined values according to the combinations defined by the lengths. + + Taking a list of values and the length of a combination, it yields each combination of length + elements taken from the values. + + Note: Beware, the number of possible combinations grows fast with the lengths. + For example, 3 out of 5 gives 10 possible combinations, but 3 out of 50 gives 19600... + + Args: + values (int | list | tuple | dict): The list of values from which build the list of + combinations. It can be either the length of a range of integers from 0, or a list of + sparse values. + length (int, optional): The length of each combination. Defaults to 2. + start (int, optional): The rank of the first combination to generate. Defaults to 0. + stop (int, optional): The rank of the last combination before what stop the generation. If + omitted, the maximum number of combination is taken. Defaults to None. + step (int, optional): The step between ranks. If start is higher than stop, the step is set + to a negative value. Defaults to 1. + offset (int, optional): An offset to add to the values if they must not start at 0. + Defaults to 0. + indexes (list | tuple, optional): A list of indexes for retrieving the values by position. + Useful if the values are not indexed by sequential numbers or with a contiguous set like a + dictionary or a spare array. Defaults to None. + + Yields: + Iterator[list]: A list of combined values by the given length. + + Examples: + ```python + from cerbernetix.toolbox.math import get_combinations + + # Get the combinations of 3 numbers from the list + values = [1, 2, 4, 8, 16] + print(list(get_combinations(values, 3))) + # [[1, 2, 4], + # [1, 2, 8], + # [1, 4, 8], + # [2, 4, 8], + # [1, 2, 16], + # [1, 4, 16], + # [2, 4, 16], + # [1, 8, 16], + # [2, 8, 16], + # [4, 8, 16]] + + # Get the combinations of 3 numbers from the list from rank 4 to 8 + values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16} + indexes = ["1", "2", "4", "8", "16"] + print(list(get_combinations(values, 3, indexes=indexes, start=4, stop=8))) + # [[1, 2, 16], + # [1, 4, 16], + # [2, 4, 16], + # [1, 8, 16]] + + # Get combinations from a number of values + print(list(get_combinations(5, 3, offset=1))) + # [[1, 2, 3], + # [1, 2, 4], + # [1, 3, 4], + # [2, 3, 4], + # [1, 2, 5], + # [1, 3, 5], + # [2, 3, 5], + # [1, 4, 5], + # [2, 4, 5], + # [3, 4, 5]] + ``` + """ + if isinstance(values, int): + nb_values = values + get_value = passthrough + else: + nb_values = len(values) + get_value = values.__getitem__ + + if indexes is None: + get_index = passthrough + else: + get_index = indexes.__getitem__ + + if nb_values == 0 or length == 0: + return + + nb_comb = comb(nb_values, length) + + if stop is None or stop > nb_comb: + stop = nb_comb + + if start >= nb_comb: + start = nb_comb - 1 + + if start < 0 or stop < -1: + raise ValueError("A combination range cannot start or stop with a negative value") + + if start > stop: + step = -abs(step) + else: + step = abs(step) + + for rank in range(start, stop, step): + combination = get_combination_from_rank(rank, length) + yield [get_value(get_index(position)) + offset for position in combination] diff --git a/src/cerbernetix/toolbox/math/utils.py b/src/cerbernetix/toolbox/math/utils.py new file mode 100644 index 0000000..cefd185 --- /dev/null +++ b/src/cerbernetix/toolbox/math/utils.py @@ -0,0 +1,68 @@ +"""A set of helper functions related to math. + +Examples +```python +from cerbernetix.toolbox.math import minmax + +mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 +``` + +```python +from cerbernetix.toolbox.math import quantity + +# Gets a size from a percentage +size = quantity(.2, 10) # 2 + +# Gets a size from an absolute value +size = quantity(6, 10) # 6 +``` +""" + + +def minmax(*args) -> tuple: + """Returns with the min and the max value from the arguments. + + Args: + *args: Arguments from which extract the min and the max. + + Returns: + tuple: A tuple with first the min value, then the max value. + + Examples + ```python + from cerbernetix.toolbox.math import minmax + + mini, maxi = minmax(3, 2, 6, 4, 5) # 2, 6 + ``` + """ + return min(*args), max(*args) + + +def quantity(quota: int | float, total: int) -> int: + """Gets a quantity with respect to a quota applied to a total. + + Args: + quota (int | float): The expected quota from the total. It can be either a percentage or an + absolute value. The percentage is represented by a number between 0 and 1. An absolute + value is represented by a number between 1 and the total included. + total (int): The total number. + + Returns: + int: The quantity computed from the quota applied to the total. It cannot exceeds the total, + and it cannot be negative. + + Examples + ```python + from cerbernetix.toolbox.math import quantity + + # Gets a size from a percentage + size = quantity(.2, 10) # 2 + + # Gets a size from an absolute value + size = quantity(6, 10) # 6 + ``` + """ + if 0 < quota < 1: + return int(total * quota) + + return min(abs(int(quota)), total) diff --git a/src/cerbernetix/toolbox/testing/test_case.py b/src/cerbernetix/toolbox/testing/test_case.py index 0fd7789..3f5c8d9 100644 --- a/src/cerbernetix/toolbox/testing/test_case.py +++ b/src/cerbernetix/toolbox/testing/test_case.py @@ -12,6 +12,7 @@ def test_dict(self): self.assertListsAlmostEqual(create_dict(), {"value": 42.4242, "PI": 3.1415}) ``` """ + import unittest from typing import Iterable @@ -19,9 +20,10 @@ def test_dict(self): class TestCase(unittest.TestCase): """Test class with additional assertions.""" + # pylint: disable-next=invalid-name def assertListsAlmostEqual( self, first: Iterable[float], second: Iterable[float], places: int = 7 - ): + ) -> None: """Asserts that 2 lists of float numbers are almost equal by the number of places. Args: @@ -46,7 +48,8 @@ def test_almost_equal(self): second_is_iterable = isinstance(second, Iterable) if not first_is_iterable and not second_is_iterable: - return self.assertAlmostEqual(first, second, places) + self.assertAlmostEqual(first, second, places) + return if not first_is_iterable or not second_is_iterable: raise AssertionError("first != second") @@ -76,9 +79,10 @@ def test_almost_equal(self): self.assertListsAlmostEqual(left, right, places) + # pylint: disable-next=invalid-name def assertListsNotAlmostEqual( self, first: Iterable[float], second: Iterable[float], places: int = 7 - ): + ) -> None: """Asserts that 2 lists of float numbers are not almost equal by the number of places. Args: diff --git a/tests/files/test_csv_file.py b/tests/files/test_csv_file.py index ad9bd67..498c3c7 100644 --- a/tests/files/test_csv_file.py +++ b/tests/files/test_csv_file.py @@ -1,10 +1,12 @@ """Test the class for reading and writing CSV files.""" + import unittest import zipfile from typing import Iterator from unittest.mock import MagicMock, Mock, patch from cerbernetix.toolbox.files import ( + CSV_AUTO, CSV_DIALECT, CSV_ENCODING, CSVFile, @@ -25,14 +27,14 @@ ["Jane", "Doe", "20", "Paris"], ] CSV_LINES_STRING = [ - '"first_name","last_name","age","city"\n', - '"John","Smith","18","London"\n', - '"Jane","Doe","20","Paris"\n', + "first_name,last_name,age,city\r\n", + "John,Smith,18,London\r\n", + "Jane,Doe,20,Paris\r\n", ] CSV_LINES_REDUCED = [ - '"first_name","last_name"\n', - '"John","Smith"\n', - '"Jane","Doe"\n', + "first_name,last_name\r\n", + "John,Smith\r\n", + "Jane,Doe\r\n", ] CSV_STRING = "".join(CSV_LINES_STRING) @@ -50,7 +52,7 @@ def test_construction_default(self): self.assertEqual(file.filename, file_path) self.assertFalse(file.binary) - self.assertEqual(file.dialect, CSV_DIALECT) + self.assertEqual(file.dialect, CSV_AUTO) self.assertEqual(file.encoding, CSV_ENCODING) self.assertIsNone(file._file) self.assertEqual(file._open_args, {"newline": ""}) @@ -230,7 +232,8 @@ def test_close_auto(self, mock_file_open): CSV_LINES_HEADLESS, ], ["list", {"fieldnames": False}, CSV_LINES_STRING, CSV_LINES_LIST], - ["auto", {"dialect": "auto"}, CSV_LINES_STRING, CSV_LINES_DICT], + ["auto", {"dialect": CSV_AUTO}, CSV_LINES_STRING, CSV_LINES_DICT], + ["dialect", {"dialect": CSV_DIALECT}, CSV_LINES_STRING, CSV_LINES_DICT], ] ) def test_read_file(self, _, params, data, expected): @@ -295,7 +298,8 @@ def test_read_file_iterator(self, mock_file_open): CSV_LINES_LIST[1:], "".join(CSV_LINES_STRING[1:]), ], - ["auto", {"dialect": "auto"}, CSV_LINES_DICT, CSV_STRING], + ["auto", {"dialect": CSV_AUTO}, CSV_LINES_DICT, CSV_STRING], + ["dialect", {"dialect": CSV_DIALECT}, CSV_LINES_DICT, CSV_STRING], ] ) def test_write_file(self, _, params, data, expected): @@ -325,17 +329,24 @@ def write(line): mock_file.write.assert_called() mock_file.close.assert_called_once() + @test_cases( + [ + [CSV_AUTO], + [CSV_DIALECT], + ] + ) @patch("builtins.open") - def test_read(self, mock_file_open): + def test_read(self, dialect, mock_file_open): """Tests a file can be read line by line.""" file_path = "/root/folder/file" mock_file = MagicMock() mock_file.close = Mock() mock_file.__iter__.return_value = CSV_LINES_STRING + mock_file.read.return_value = CSV_STRING mock_file_open.return_value = mock_file - file = CSVFile(file_path) + file = CSVFile(file_path, dialect=dialect) self.assertRaises(ValueError, file.read) @@ -426,6 +437,7 @@ def test_iterator(self, mock_file_open): mock_file = MagicMock() mock_file.close = Mock() mock_file.__iter__.return_value = CSV_LINES_STRING + mock_file.read.return_value = CSV_STRING mock_file_open.return_value = mock_file file = CSVFile(file_path) @@ -456,6 +468,7 @@ def test_read_csv_file(self, mock_file_open): mock_file = MagicMock() mock_file.close = Mock() mock_file.__iter__.return_value = CSV_LINES_STRING + mock_file.read.return_value = CSV_STRING mock_file_open.return_value = mock_file result = read_csv_file(file_path) @@ -473,6 +486,7 @@ def test_read_csv_file_iterator(self, mock_file_open): mock_file = MagicMock() mock_file.close = Mock() mock_file.__iter__.return_value = CSV_LINES_STRING + mock_file.read.return_value = CSV_STRING mock_file_open.return_value = mock_file result = read_csv_file(file_path, iterator=True) @@ -538,8 +552,8 @@ def write(line): CSV_LINES_LIST, ], [ - "dialect auto", - {"dialect": "auto"}, + "dialect", + {"dialect": CSV_DIALECT}, "FOO.CSV", CSV_STRING, CSV_LINES_DICT, diff --git a/tests/files/test_json_file.py b/tests/files/test_json_file.py index 6d7c2ab..2b42e59 100644 --- a/tests/files/test_json_file.py +++ b/tests/files/test_json_file.py @@ -1,10 +1,16 @@ """Test the class for reading and writing JSON files.""" + import unittest from unittest.mock import Mock, patch from cerbernetix.toolbox.files import ( JSON_ENCODING, + JSON_ENSURE_ASCII, JSON_INDENT, + JSON_SEPARATORS, + JSON_SKIP_KEYS, + JSON_SORT_KEYS, + JSON_STRICT, JSONFile, read_json_file, write_json_file, @@ -12,6 +18,16 @@ JSON_DATA = {"name": "test", "level": 20, "keywords": ["one", "two"], "enabled": True} JSON_STRING = """{ + "name": "test", + "level": 20, + "keywords": [ + "one", + "two" + ], + "enabled": true +}""" +JSON_STRING_PACKED = """{"name":"test","level":20,"keywords":["one","two"],"enabled":true}""" +JSON_STRING_SORTED = """{ "enabled": true, "keywords": [ "one", @@ -36,6 +52,11 @@ def test_construction_default(self): self.assertEqual(file.filename, file_path) self.assertFalse(file.binary) self.assertEqual(file.indent, JSON_INDENT) + self.assertEqual(file.separators, JSON_SEPARATORS) + self.assertEqual(file.sort_keys, JSON_SORT_KEYS) + self.assertEqual(file.skip_keys, JSON_SKIP_KEYS) + self.assertEqual(file.ensure_ascii, JSON_ENSURE_ASCII) + self.assertEqual(file.strict, JSON_STRICT) self.assertEqual(file.encoding, JSON_ENCODING) self.assertIsNone(file._file) self.assertEqual(file._open_args, {}) @@ -45,13 +66,33 @@ def test_construction_params(self): file_path = "/root/folder/file" encoding = "ascii" indent = 2 + separators = (",", ":") + sort_keys = True + skip_keys = True + ensure_ascii = True + strict = True newline = "\n" - file = JSONFile(file_path, encoding=encoding, indent=indent, newline=newline) + file = JSONFile( + file_path, + encoding=encoding, + indent=indent, + separators=separators, + sort_keys=sort_keys, + skip_keys=skip_keys, + ensure_ascii=ensure_ascii, + strict=strict, + newline=newline, + ) self.assertEqual(file.filename, file_path) self.assertFalse(file.binary) self.assertEqual(file.indent, indent) + self.assertEqual(file.separators, separators) + self.assertEqual(file.sort_keys, sort_keys) + self.assertEqual(file.skip_keys, skip_keys) + self.assertEqual(file.ensure_ascii, ensure_ascii) + self.assertEqual(file.strict, strict) self.assertEqual(file.encoding, encoding) self.assertIsNone(file._file) self.assertEqual(file._open_args, {"newline": newline}) @@ -217,6 +258,44 @@ def test_write_file(self, mock_file_open): mock_file.write.assert_called_with(JSON_STRING) mock_file.close.assert_called_once() + @patch("builtins.open") + def test_write_file_sorted_keys(self, mock_file_open): + """Tests a file can be written at once.""" + file_path = "/root/folder/file" + + count = len(JSON_STRING) + mock_file = Mock() + mock_file.write = Mock(return_value=count) + mock_file.close = Mock() + mock_file_open.return_value = mock_file + + file = JSONFile(file_path, sort_keys=True) + + self.assertEqual(file.write_file(JSON_DATA), count) + + mock_file_open.assert_called_once() + mock_file.write.assert_called_with(JSON_STRING_SORTED) + mock_file.close.assert_called_once() + + @patch("builtins.open") + def test_write_file_packed(self, mock_file_open): + """Tests a file can be written at once.""" + file_path = "/root/folder/file" + + count = len(JSON_STRING) + mock_file = Mock() + mock_file.write = Mock(return_value=count) + mock_file.close = Mock() + mock_file_open.return_value = mock_file + + file = JSONFile(file_path, indent=None, separators=(",", ":")) + + self.assertEqual(file.write_file(JSON_DATA), count) + + mock_file_open.assert_called_once() + mock_file.write.assert_called_with(JSON_STRING_PACKED) + mock_file.close.assert_called_once() + @patch("builtins.open") def test_read(self, mock_file_open): """Tests a file can be read.""" diff --git a/tests/math/test_combination.py b/tests/math/test_combination.py index 2adcd2c..41316b8 100644 --- a/tests/math/test_combination.py +++ b/tests/math/test_combination.py @@ -1,7 +1,13 @@ """Test the set of functions for working with combinations.""" + import unittest +from typing import Iterator -from cerbernetix.toolbox.math import get_combination_from_rank, get_combination_rank +from cerbernetix.toolbox.math import ( + get_combination_from_rank, + get_combination_rank, + get_combinations, +) from cerbernetix.toolbox.testing import test_cases @@ -205,3 +211,138 @@ def test_get_combination_from_rank_error(self): """Test get_combination_from_rank errors.""" self.assertRaises(ValueError, lambda: get_combination_from_rank(-1, 2)) self.assertRaises(ValueError, lambda: get_combination_from_rank(0, -1, 4)) + + def test_get_combinations_int(self): + """Test get_combinations.""" + self.assertIsInstance(get_combinations(5, 3), Iterator) + self.assertEqual(next(get_combinations(5, 3)), [0, 1, 2]) + self.assertEqual( + list(get_combinations(5, 3)), + [ + [0, 1, 2], + [0, 1, 3], + [0, 2, 3], + [1, 2, 3], + [0, 1, 4], + [0, 2, 4], + [1, 2, 4], + [0, 3, 4], + [1, 3, 4], + [2, 3, 4], + ], + ) + + def test_get_combinations_offset(self): + """Test get_combinations.""" + self.assertIsInstance(get_combinations(5, 3, offset=1), Iterator) + self.assertEqual(next(get_combinations(5, 3, offset=1)), [1, 2, 3]) + self.assertEqual( + list(get_combinations(5, 3, offset=1)), + [ + [1, 2, 3], + [1, 2, 4], + [1, 3, 4], + [2, 3, 4], + [1, 2, 5], + [1, 3, 5], + [2, 3, 5], + [1, 4, 5], + [2, 4, 5], + [3, 4, 5], + ], + ) + + def test_get_combinations_values(self): + """Test get_combinations.""" + values = [1, 2, 4, 8, 16] + self.assertIsInstance(get_combinations(values, 3), Iterator) + self.assertEqual(next(get_combinations(values, 3)), [1, 2, 4]) + self.assertEqual( + list(get_combinations(values, 3)), + [ + [1, 2, 4], + [1, 2, 8], + [1, 4, 8], + [2, 4, 8], + [1, 2, 16], + [1, 4, 16], + [2, 4, 16], + [1, 8, 16], + [2, 8, 16], + [4, 8, 16], + ], + ) + + def test_get_combinations_indexes(self): + """Test get_combinations with a dictionary and a list of indexes.""" + values = {"1": 1, "2": 2, "4": 4, "8": 8, "16": 16} + indexes = ["1", "2", "4", "8", "16"] + self.assertIsInstance(get_combinations(values, 3, indexes=indexes), Iterator) + self.assertEqual(next(get_combinations(values, 3, indexes=indexes)), [1, 2, 4]) + self.assertEqual( + list(get_combinations(values, 3, indexes=indexes)), + [ + [1, 2, 4], + [1, 2, 8], + [1, 4, 8], + [2, 4, 8], + [1, 2, 16], + [1, 4, 16], + [2, 4, 16], + [1, 8, 16], + [2, 8, 16], + [4, 8, 16], + ], + ) + + def test_get_combinations_range(self): + """Test get_combinations with a range.""" + values = [1, 2, 4, 8, 16] + self.assertIsInstance(get_combinations(values, 3, start=2, stop=8, step=2), Iterator) + self.assertEqual( + list(get_combinations(values, 3, start=2, stop=8, step=2)), + [ + [1, 4, 8], + [1, 2, 16], + [2, 4, 16], + ], + ) + self.assertEqual( + list(get_combinations(values, 3, start=8, stop=2, step=2)), + [ + [2, 8, 16], + [2, 4, 16], + [1, 2, 16], + ], + ) + self.assertEqual( + list(get_combinations(values, 3, start=20, stop=-1)), + [ + [4, 8, 16], + [2, 8, 16], + [1, 8, 16], + [2, 4, 16], + [1, 4, 16], + [1, 2, 16], + [2, 4, 8], + [1, 4, 8], + [1, 2, 8], + [1, 2, 4], + ], + ) + self.assertEqual( + list(get_combinations(values, 3, start=20)), + [ + [4, 8, 16], + ], + ) + + def test_get_combinations_empty(self): + """Test get_combinations returns empty.""" + self.assertEqual(list(get_combinations(0, 3)), []) + self.assertEqual(list(get_combinations(5, 0)), []) + + def test_get_combinations_error(self): + """Test get_combinations errors.""" + self.assertRaises(ValueError, lambda: next(get_combinations(5, start=-10))) + self.assertRaises(ValueError, lambda: next(get_combinations(5, stop=-10))) diff --git a/tests/math/test_utils.py b/tests/math/test_utils.py new file mode 100644 index 0000000..3b73efb --- /dev/null +++ b/tests/math/test_utils.py @@ -0,0 +1,28 @@ +"""Test the set of helper functions related to math.""" + +import unittest + +from cerbernetix.toolbox.math import minmax, quantity + + +class TestUtils(unittest.TestCase): + """Test suite for the set of helper functions related to math.""" + + def test_minmax(self): + """Test minmax.""" + self.assertEqual(minmax([1]), (1, 1)) + self.assertEqual(minmax([1, 2]), (1, 2)) + self.assertEqual(minmax([2, 1]), (1, 2)) + self.assertEqual(minmax(*[1, 2]), (1, 2)) + self.assertEqual(minmax(*[2, 1]), (1, 2)) + self.assertEqual(minmax(1, 2), (1, 2)) + self.assertEqual(minmax(2, 1), (1, 2)) + self.assertEqual(minmax(3, 2, 6, 5, 4), (2, 6)) + + def test_quantity(self): + """Test quantity.""" + self.assertEqual(quantity(5, 10), 5) + self.assertEqual(quantity(0.1, 10), 1) + self.assertEqual(quantity(1.5, 10), 1) + self.assertEqual(quantity(-0.2, 10), 0) + self.assertEqual(quantity(30, 10), 10)