Compare commits

...

75 Commits

Author SHA1 Message Date
Clemens Schwaighofer
d098eb58f3 v0.48.0: Update Caller class with better error handling and reporting 2026-01-30 18:20:21 +09:00
Clemens Schwaighofer
5319a059ad Update the caller class
- has now ErrorResponse return values instead of None on errors
- changed parameter cafile to ca_file and its position in the init method
- Proxy has ProxyConfig Typed Dict format

Tests updates to reflect those changes
2026-01-30 18:17:41 +09:00
Clemens Schwaighofer
163b8c4018 Update caller Class, backport from github manage script 2026-01-30 17:32:30 +09:00
Clemens Schwaighofer
6322b95068 v0.47.0: fingerprint update with fallback for str/int index overlaps 2026-01-27 17:15:32 +09:00
Clemens Schwaighofer
715ed1f9c2 Docblocks update in in iterator handling fingerprint 2026-01-27 17:14:31 +09:00
Clemens Schwaighofer
82a759dd21 Fix fingerprint with mixed int and str keys
Create a fallback hash function to handle mixed key types in dictionaries
and lists, ensuring consistent hashing across different data structures.

Fallback called is prefixed with "HO_" to indicate its usage.
2026-01-27 15:59:38 +09:00
Clemens Schwaighofer
fe913608c4 Fix iteration list helpers dict list type 2026-01-27 14:52:11 +09:00
Clemens Schwaighofer
79f9c5d1c6 iterator list helpers tests run cases updated 2026-01-27 14:51:25 +09:00
Clemens Schwaighofer
3d091129e2 v0.46.0: Add unique list helper function 2026-01-27 14:43:35 +09:00
Clemens Schwaighofer
1a978f786d Add a list helper to create unique list of dictionaries and tests for it. 2026-01-27 14:42:19 +09:00
Clemens Schwaighofer
51669d3c5f Settings loader test-run add boolean convert check test 2026-01-23 18:07:52 +09:00
Clemens Schwaighofer
d128dcb479 v0.45.1: Fix Log with log console format set to None 2026-01-23 15:16:38 +09:00
Clemens Schwaighofer
84286593f6 Log fix bug where log consosle format set to None would throw an exception
Also add prefix "[SettingsLoader] " to print statements in SettingsLoader if we do not write to log
2026-01-23 15:14:31 +09:00
Clemens Schwaighofer
8d97f09e5e v0.45.0: Log add function to get console formatter flags set 2026-01-23 11:37:02 +09:00
Clemens Schwaighofer
2748bc19be Log, add get console formatter method
Returns current flags set for console formatter
2026-01-23 11:33:38 +09:00
Clemens Schwaighofer
0b3c8fc774 v0.44.2: Move the compiled regex into dedicated file 2026-01-09 16:17:27 +09:00
Clemens Schwaighofer
7da18e0f00 Moved the compiled regex patterns to a new file regex_constants_compiled
So we do not force the compiled build if not needed
2026-01-09 16:15:38 +09:00
Clemens Schwaighofer
49e38081ad v0.44.1: add pre compiled regexes 2026-01-08 15:16:26 +09:00
Clemens Schwaighofer
a14f993a31 Add pre-compiled REGEX entries to the regex pattern file
compiled ones hare prefixed with COMPILED_
2026-01-08 15:14:48 +09:00
Clemens Schwaighofer
ae938f9909 v0.44.0: Add more REGEX patters for email matching 2026-01-08 14:59:49 +09:00
Clemens Schwaighofer
f91e0bb93a Add new regex constants for email handling and update related tests 2026-01-08 14:58:14 +09:00
Clemens Schwaighofer
d3f61005cf v0.43.4: Fix for config loader with empty to split into lists values 2026-01-06 10:04:03 +09:00
Clemens Schwaighofer
2923a3e88b Fix settings loader to return empty list when splitting empty string value 2026-01-06 09:58:21 +09:00
Clemens Schwaighofer
a73ced0067 v0.43.3: settings loader raise exception and log message text split 2025-12-24 10:25:42 +09:00
Clemens Schwaighofer
f89b91fe7f Settings loader different log string to value error raise string 2025-12-24 10:23:27 +09:00
Clemens Schwaighofer
5950485d46 v0.43.2: add error message list reset to settings loader 2025-12-24 10:18:54 +09:00
Clemens Schwaighofer
f349927a63 Reset error message list in settings loader 2025-12-24 10:14:54 +09:00
Clemens Schwaighofer
dfe8890598 v0.43.1: settings loader update for error reporting on exception raise 2025-12-24 10:09:53 +09:00
Clemens Schwaighofer
d224876a8e Settings loader, pass error messages to exception raise
So we can get the actual error message in the exception if logging is all off
2025-12-24 10:08:38 +09:00
Clemens Schwaighofer
17e8c76b94 v0.43.0: SQLmain wrapper class, math helper functions 2025-12-18 17:24:05 +09:00
Clemens Schwaighofer
9034a31cd6 Add math helper module
Currently with GCD and LCD functions, along with unit tests.
2025-12-18 17:21:14 +09:00
Clemens Schwaighofer
523e61c9f7 Add SQL Main class as general wrapper for SQL DB handling 2025-12-18 17:20:57 +09:00
Clemens Schwaighofer
cf575ded90 Update on the CSV helper class with UTF detection for BOM reading 2025-12-16 18:53:16 +09:00
Clemens Schwaighofer
11a75d8532 Settings loader error message text update 2025-12-16 09:47:40 +09:00
Clemens Schwaighofer
6593e11332 Update deprecation infor for enum base
Test run add for regex checks domain name regex contants
2025-12-10 11:35:00 +09:00
Clemens Schwaighofer
c310f669d6 v0.42.2: log class update with method to check if any handler is a given minimum level 2025-12-04 14:41:47 +09:00
Clemens Schwaighofer
f327f47c3f Add uv.lock to gitignore file 2025-12-04 14:41:04 +09:00
Clemens Schwaighofer
acd61e825e Add Log method "any handler is minimum level" with tests
Checks if a given handler is set for any current active handler
2025-12-04 14:37:55 +09:00
Clemens Schwaighofer
895701da59 v0.42.1: add requests socks 2025-11-20 11:41:11 +09:00
Clemens Schwaighofer
e0fb0db1f0 Add requets socks access 2025-11-20 11:40:21 +09:00
Clemens Schwaighofer
dc7e56106e v0.42.0: Move text colors to external lib and depreacte the ones in corelibs collection 2025-11-20 11:05:34 +09:00
Clemens Schwaighofer
90e5179980 Remove text color handling from corelibs and use corelibs_text_colors instead
Also update enum with proper pyi file for deprecation warnings
2025-11-20 10:59:44 +09:00
Clemens Schwaighofer
9db39003c4 v0.41.0: settings parsers, make arguments override no longer automatic 2025-11-20 10:11:41 +09:00
Clemens Schwaighofer
4ffe372434 Change that the args overload has to be set to override settings from arguments
So we do not have issues with values change because an arugment has the same name as a setting name
2025-11-20 10:00:36 +09:00
Clemens Schwaighofer
a00c27c465 v0.40.0: Fix for settings loader with arguments 2025-11-19 19:03:35 +09:00
Clemens Schwaighofer
1f7f4b8d53 Update settings loader with skip argument set if not matching settings type or ignore flag is set
We have "args:no" that can be set to avoid override from arguments.
Also arguments that do not match the exepected type are not loaded
2025-11-19 19:01:29 +09:00
Clemens Schwaighofer
baca79ce82 v0.39.2: [Fix] Skip Log format update if it did not change 2025-11-19 17:45:50 +09:00
Clemens Schwaighofer
4265be6430 Merge branch 'development' 2025-11-19 17:45:08 +09:00
Clemens Schwaighofer
c16b086467 v0.39.1: Skip Log format update if it did not change 2025-11-19 17:44:44 +09:00
Clemens Schwaighofer
48a98c0206 Merge branch 'master' into development 2025-11-19 17:43:13 +09:00
Clemens Schwaighofer
f1788f057f Log skip format change it format flags have not changed 2025-11-19 17:42:47 +09:00
Clemens Schwaighofer
0ad8883809 v0.39.0: Add Log LEVEL flag for console format 2025-11-19 17:37:00 +09:00
Clemens Schwaighofer
51e9b1ce7c Add "LEVEL" option to console log format
So we can set output to onle the message without any information (NONE),
only level (BARE), time and level (MINIMAL), time, file, line and level (CONDENSED) or
(ALL) full information.
2025-11-19 17:35:27 +09:00
Clemens Schwaighofer
0d3104f60a v0.38.0: Log console format update 2025-11-19 15:45:49 +09:00
Clemens Schwaighofer
d29f827fc9 Add a function to Log system to update the console formatter dynamically. 2025-11-19 15:17:25 +09:00
Clemens Schwaighofer
282fe1f7c0 v0.37.0: Log add from lookup for strings in Console config, move var helpers, datetime, enum to stand alone libs 2025-11-19 13:48:29 +09:00
Clemens Schwaighofer
afce5043e4 Cleanup other functions to use extern corelibs
Remove tests for parts that have moved to stand alone libraries
2025-11-19 13:46:34 +09:00
Clemens Schwaighofer
5996bb1fc0 Add Log ConsoleFormatSettings.from_string static method to get settings by name with default option
To help set from config or command line with fallback
2025-11-19 13:45:26 +09:00
Clemens Schwaighofer
06a17d7c30 Switch datetime handling, var handling to corelibs libraries
Use external corelib libraries for datetime handling and var handling enum base.
2025-11-19 13:13:32 +09:00
Clemens Schwaighofer
af7633183c v0.36.0: Log console format settings with bitwise mask 2025-11-19 11:31:50 +09:00
Clemens Schwaighofer
1280b2f855 Log switch to bitwise flag settings for console format type
Has the following settings
TIME, TIME_SECONDS, TIME_MILLISECONDS, TIME_MICROSECONDS: enable time output in different formats
TIME and TIME_MILLISECONDS are equivalent, if multiple are set the smallest precision wins
TIMEZONE: add time zone to time output
NAME: log group name
FILE: short file name
FUNCTION: function name
LINENO: line number

There is a class with quick grouped settings
ConsoleFormatSettings
ALL: all options enabled, time is in milliseconds
CONDENSED: time without time zone, file and line number
MINIMAL: only time without time zone
BARE: only the message, no other info
2025-11-19 11:25:49 +09:00
Clemens Schwaighofer
2e0b1f5951 v0.35.2: Sync miss for Log file format change 2025-11-18 15:55:49 +09:00
Clemens Schwaighofer
548d7491b8 Merge branch 'development' 2025-11-18 15:55:05 +09:00
Clemens Schwaighofer
ad99115544 v0.35.1: Log move pid into path name block, remove double filename 2025-11-18 15:51:29 +09:00
Clemens Schwaighofer
52919cbc49 Log: move process id to front of pathname in log format
The previous filename:pid has been removed, the filename is part of the pathname.
No need for double filename info and wasting space in the log line.
2025-11-18 15:49:46 +09:00
Clemens Schwaighofer
7f2dc13c31 v0.35.0: Logging update with output format settings for console logging 2025-11-18 15:37:13 +09:00
Clemens Schwaighofer
592652cff1 Update logging with console output format changes
"console_format_type" with "normal", "condensed", "minimal" options
This sets the format of the console output, controlling the amount of detail shown.
normal show log title, file, function and line number
condensed show file and line number only
minimal shows only timestamp, log level and message
Default is normal

"console_iso_precision" with "seconds", "milliseconds", "microseconds" options
This sets the precision of the ISO timestamp in console logs.
Default is milliseconds

The timestamp output is now ISO8601 formatatted with time zone.
2025-11-18 15:31:16 +09:00
Clemens Schwaighofer
6a1724695e Fix pyproject settings by removing explicit=true 2025-11-11 18:05:07 +09:00
Clemens Schwaighofer
037210756e v0.34.0: add BOM check for files 2025-11-06 18:22:45 +09:00
Clemens Schwaighofer
4e78d83092 Add checks for BOM encoding in files 2025-11-06 18:21:32 +09:00
Clemens Schwaighofer
0e6331fa6a v0.33.0: datetime parsing update 2025-11-06 13:26:07 +09:00
Clemens Schwaighofer
c98c5df63c Update datetime parse helper
Allow non T in isotime format, add non T normal datetime parsing
2025-11-06 13:24:27 +09:00
Clemens Schwaighofer
0981c74da9 v0.32.0: add email sending 2025-10-27 11:22:11 +09:00
Clemens Schwaighofer
31518799f6 README update 2025-10-27 11:20:46 +09:00
Clemens Schwaighofer
e8b4b9b48e Add send email class 2025-10-27 11:19:38 +09:00
65 changed files with 5650 additions and 4189 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
.mypy_cache/
**/.env
.coverage
uv.lock

View File

@@ -4,12 +4,14 @@
> This is pre-production, location of methods and names of paths can change
>
> This will be split up into modules per file and this will be just a collection holder
> See [Deprecated](#deprecated) below
This is a pip package that can be installed into any project and covers the following parts
- logging update with exception logs
- requests wrapper for easier auth pass on access
- dict fingerprinting
- sending email
- jmespath search
- json helpers for conten replace and output
- dump outputs for data for debugging
@@ -26,6 +28,7 @@ This is a pip package that can be installed into any project and covers the foll
- debug_handling: various debug helpers like data dumper, timer, utilization, etc
- db_handling: SQLite interface class
- encyption_handling: symmetric encryption
- email_handling: simple email sending
- file_handling: crc handling for file content and file names, progress bar
- json_handling: jmespath support and json date support, replace content in dict with json paths
- iterator_handling: list and dictionary handling support (search, fingerprinting, etc)
@@ -40,6 +43,15 @@ This is a pip package that can be installed into any project and covers the foll
- csv_handling/csv_interface: The CSV DictWriter interface is just in a very basic way implemented
- script_handling/script_helpers: No idea if there is need for this, tests are written but not finished
## Deprecated
All content in this module will move to stand alone libraries, as of now the following entries have moved and will throw deprecated warnings if used
- var_handling.enum_base: corelibs-enum-base
- var_handling.var_helpers: corelibs-var
- datetime_handling: corelibs-datetime
- string_handling.text_colors: corelibs-text-colors
## UV setup
uv must be [installed](https://docs.astral.sh/uv/getting-started/installation/)
@@ -50,7 +62,7 @@ Have the following setup in `project.toml`
```toml
[[tool.uv.index]]
name = "egra-gitea"
name = "opj-pypi"
url = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/"
publish-url = "https://git.egplusww.jp/api/packages/PyPI/pypi"
explicit = true
@@ -58,15 +70,15 @@ explicit = true
```sh
uv build
uv publish --index egra-gitea --token <gitea token>
uv publish --index opj-pypi --token <gitea token>
```
## Test package
## Use package
We must set the full index URL here because we run with "--no-project"
```sh
uv run --with corelibs --index egra-gitea=https://git.egplusww.jp/api/packages/PyPI/pypi/simple/ --no-project -- python -c "import corelibs"
uv run --with corelibs --index opj-pypi=https://git.egplusww.jp/api/packages/PyPI/pypi/simple/ --no-project -- python -c "import corelibs"
```
### Python tests
@@ -99,7 +111,7 @@ uv run test-run/<script>
This will also add the index entry
```sh
uv add corelibs --index egra-gitea=https://git.egplusww.jp/api/packages/PyPI/pypi/simple/
uv add corelibs --index opj-pypi=https://git.egplusww.jp/api/packages/PyPI/pypi/simple/
```
## Python venv setup

View File

@@ -1,37 +1,47 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.31.1"
version = "0.48.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-datetime>=1.0.1",
"corelibs-enum-base>=1.0.0",
"corelibs-text-colors>=1.0.0",
"corelibs-var>=1.0.0",
"cryptography>=46.0.3",
"jmespath>=1.0.1",
"jsonpath-ng>=1.7.0",
"psutil>=7.0.0",
"requests>=2.32.4",
"requests[socks]>=2.32.5",
]
# set this to disable publish to pypi (pip)
# classifiers = ["Private :: Do Not Upload"]
# MARK: build target
[[tool.uv.index]]
name = "egra-gitea"
url = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/"
publish-url = "https://git.egplusww.jp/api/packages/PyPI/pypi"
explicit = true
# MARK: build system
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# set this to disable publish to pypi (pip)
# classifiers = ["Private :: Do Not Upload"]
# MARK: build target
[[tool.uv.index]]
name = "opj-pypi"
url = "https://git.egplusww.jp/api/packages/PyPI/pypi/simple/"
publish-url = "https://git.egplusww.jp/api/packages/PyPI/pypi"
[tool.uv.sources]
corelibs-enum-base = { index = "opj-pypi" }
corelibs-datetime = { index = "opj-pypi" }
corelibs-var = { index = "opj-pypi" }
corelibs-text-colors = { index = "opj-pypi" }
[dependency-groups]
dev = [
"deepdiff>=8.6.1",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"typing-extensions>=4.15.0",
]
# MARK: Python linting
@@ -63,12 +73,13 @@ ignore = [
[tool.pylint.MASTER]
# this is for the tests/etc folders
init-hook='import sys; sys.path.append("src/")'
# MARK: Testing
[tool.pytest.ini_options]
testpaths = [
"tests",
]
[tool.coverage.run]
omit = [
"*/tests/*",

View File

@@ -19,9 +19,26 @@ def compile_re(reg: str) -> re.Pattern[str]:
# email regex
EMAIL_BASIC_REGEX: str = r"""
^[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}
@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}$
SUB_EMAIL_BASIC_REGEX: str = r"""
[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~][A-Za-z0-9!#$%:\(\)&'*+\-\/=?^_`{|}~\.]{0,63}
@(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[a-zA-Z]{2,6}
"""
EMAIL_BASIC_REGEX = rf"^{SUB_EMAIL_BASIC_REGEX}$"
# name + email regex for email sending type like "foo bar" <email@mail.com>
NAME_EMAIL_SIMPLE_REGEX = r"""
^\s*(?:"(?P<name1>[^"]+)"\s*<(?P<email1>[^>]+)>|
(?P<name2>.+?)\s*<(?P<email2>[^>]+)>|
<(?P<email3>[^>]+)>|
(?P<email4>[^\s<>]+))\s*$
"""
# name + email with the basic regex set
NAME_EMAIL_BASIC_REGEX = rf"""
^\s*(?:
"(?P<name1>[^"]+)"\s*<(?P<email1>{SUB_EMAIL_BASIC_REGEX})>|
(?P<name2>.+?)\s*<(?P<email2>{SUB_EMAIL_BASIC_REGEX})>|
<(?P<email3>{SUB_EMAIL_BASIC_REGEX})>|
(?P<email4>{SUB_EMAIL_BASIC_REGEX})
)\s*$
"""
# Domain regex with localhost
DOMAIN_WITH_LOCALHOST_REGEX: str = r"""

View File

@@ -0,0 +1,23 @@
"""
List of regex compiled strings that can be used
"""
from corelibs.check_handling.regex_constants import (
compile_re,
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX
)
# all above in compiled form
COMPILED_EMAIL_BASIC_REGEX = compile_re(EMAIL_BASIC_REGEX)
COMPILED_NAME_EMAIL_SIMPLE_REGEX = compile_re(NAME_EMAIL_SIMPLE_REGEX)
COMPILED_NAME_EMAIL_BASIC_REGEX = compile_re(NAME_EMAIL_BASIC_REGEX)
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX = compile_re(DOMAIN_WITH_LOCALHOST_REGEX)
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX = compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX)
COMPILED_DOMAIN_REGEX = compile_re(DOMAIN_REGEX)
# __END__

View File

@@ -8,9 +8,9 @@ import re
import configparser
from typing import Any, Tuple, Sequence, cast
from pathlib import Path
from corelibs_var.var_helpers import is_int, is_float, str_to_bool
from corelibs.logging_handling.log import Log
from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list
from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool
from corelibs.config_handling.settings_loader_handling.settings_loader_check import SettingsLoaderCheck
@@ -53,6 +53,9 @@ class SettingsLoader:
# for check settings, abort flag
self.__check_settings_abort: bool = False
# error messages for raise ValueError
self.__error_msg: list[str] = []
# MARK: load settings
def load_settings(
self,
@@ -87,12 +90,16 @@ class SettingsLoader:
Returns:
dict[str, str]: key = value list
"""
# reset error message list before run
self.__error_msg = []
# default set entries
entry_set_empty: dict[str, str | None] = {}
# entries that have to be split
entry_split_char: dict[str, str] = {}
# entries that should be converted
entry_convert: dict[str, str] = {}
# no args to set
args_overrride: list[str] = []
# all the settings for the config id given
settings: dict[str, dict[str, Any]] = {
config_id: {},
@@ -107,7 +114,7 @@ class SettingsLoader:
if allow_not_exist is True:
return {}
raise ValueError(self.__print(
f"[!] Cannot read [{config_id}] block in the {self.config_file}: {e}",
f"[!] Cannot read [{config_id}] block in the file {self.config_file}: {e}",
'CRITICAL'
)) from e
try:
@@ -162,12 +169,17 @@ class SettingsLoader:
f"[!] In [{config_id}] the split character setup for entry failed: {check}: {e}",
'CRITICAL'
)) from e
if check == "args_override:yes":
args_overrride.append(key)
if skip:
continue
settings[config_id][key] = [
__value.replace(" ", "")
for __value in settings[config_id][key].split(split_char)
]
if settings[config_id][key]:
settings[config_id][key] = [
__value.replace(" ", "")
for __value in settings[config_id][key].split(split_char)
]
else:
settings[config_id][key] = []
except KeyError as e:
raise ValueError(self.__print(
f"[!] Cannot read [{config_id}] block because the entry [{e}] could not be found",
@@ -177,17 +189,23 @@ class SettingsLoader:
# ignore error if arguments are set
if not self.__check_arguments(config_validate, True):
raise ValueError(self.__print(f"[!] Cannot find file: {self.config_file}", 'CRITICAL'))
else:
# base set
settings[config_id] = {}
# base set
settings[config_id] = {}
# make sure all are set
# if we have arguments set, this override config settings
error: bool = False
for entry, validate in config_validate.items():
# if we have command line option set, this one overrides config
if self.__get_arg(entry):
if (args_entry := self.__get_arg(entry)) is not None:
self.__print(f"[*] Command line option override for: {entry}", 'WARNING')
settings[config_id][entry] = self.args.get(entry)
if (
# only set if flagged as allowed override from args
entry in args_overrride and
(isinstance(args_entry, list) and entry_split_char.get(entry)) or
(not isinstance(args_entry, list) and not entry_split_char.get(entry))
):
# args is list, but entry has not split, do not set
settings[config_id][entry] = args_entry
# validate checks
for check in validate:
# CHECKS
@@ -263,7 +281,10 @@ class SettingsLoader:
error = True
self.__print(f"[!] Missing content entry for: {entry}", 'ERROR')
if error is True:
raise ValueError(self.__print("[!] Missing or incorrect settings data. Cannot proceed", 'CRITICAL'))
self.__print("[!] Missing or incorrect settings data. Cannot proceed", 'CRITICAL')
raise ValueError(
"Missing or incorrect settings data. Cannot proceed: " + "; ".join(self.__error_msg)
)
# set empty
for [entry, empty_set] in entry_set_empty.items():
# if set, skip, else set to empty value
@@ -277,10 +298,8 @@ class SettingsLoader:
elif convert_type in ["float", "any"] and is_float(settings[config_id][entry]):
settings[config_id][entry] = float(settings[config_id][entry])
elif convert_type in ["bool", "any"] and (
settings[config_id][entry] == "true" or
settings[config_id][entry] == "True" or
settings[config_id][entry] == "false" or
settings[config_id][entry] == "False"
settings[config_id][entry].lower() == "true" or
settings[config_id][entry].lower() == "false"
):
try:
settings[config_id][entry] = str_to_bool(settings[config_id][entry])
@@ -558,7 +577,10 @@ class SettingsLoader:
self.log.logger.log(Log.get_log_level_int(level), msg, stacklevel=2)
if self.log is None or self.always_print:
if print_error:
print(msg)
print(f"[SettingsLoader] {msg}")
if level == 'ERROR':
# remove any prefix [!] for error message list
self.__error_msg.append(msg.replace('[!] ', '').strip())
return msg

View File

@@ -7,10 +7,13 @@ from typing import Any, Sequence
from pathlib import Path
from collections import Counter
import csv
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
from corelibs.exceptions.csv_exceptions import (
NoCsvReader, CompulsoryCsvHeaderCheckFailed, CsvHeaderDataMissing
)
ENCODING = 'utf-8'
ENCODING_UTF8_SIG = 'utf-8-sig'
DELIMITER = ","
QUOTECHAR = '"'
# type: _QuotingType
@@ -27,6 +30,7 @@ class CsvWriter:
file_name: Path,
header_mapping: dict[str, str],
header_order: list[str] | None = None,
encoding: str = ENCODING,
delimiter: str = DELIMITER,
quotechar: str = QUOTECHAR,
quoting: Any = QUOTING,
@@ -38,6 +42,7 @@ class CsvWriter:
self.__delimiter = delimiter
self.__quotechar = quotechar
self.__quoting = quoting
self.__encoding = encoding
self.csv_file_writer = self.__open_csv(header_order)
def __open_csv(self, header_order: list[str] | None) -> csv.DictWriter[str]:
@@ -69,7 +74,8 @@ class CsvWriter:
try:
fp = open(
self.__file_name,
"w", encoding="utf-8"
"w",
encoding=self.__encoding
)
csv_file_writer = csv.DictWriter(
fp,
@@ -109,6 +115,7 @@ class CsvReader:
self,
file_name: Path,
header_check: Sequence[str] | None = None,
encoding: str = ENCODING,
delimiter: str = DELIMITER,
quotechar: str = QUOTECHAR,
quoting: Any = QUOTING,
@@ -118,6 +125,7 @@ class CsvReader:
self.__delimiter = delimiter
self.__quotechar = quotechar
self.__quoting = quoting
self.__encoding = encoding
self.header: Sequence[str] | None = None
self.csv_file_reader = self.__open_csv()
@@ -129,9 +137,16 @@ class CsvReader:
csv.DictReader | None: _description_
"""
try:
# if UTF style check if this is BOM
if self.__encoding.lower().startswith('utf-') and is_bom_encoded(self.__file_name):
bom_info = is_bom_encoded_info(self.__file_name)
if bom_info['encoding'] == 'utf-8':
self.__encoding = ENCODING_UTF8_SIG
else:
self.__encoding = bom_info['encoding'] or self.__encoding
fp = open(
self.__file_name,
"r", encoding="utf-8"
"r", encoding=self.__encoding
)
csv_file_reader = csv.DictReader(
fp,

View File

@@ -2,26 +2,13 @@
Various string based date/time helpers
"""
import time as time_t
from datetime import datetime, time
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from typing import Callable
DAYS_OF_WEEK_LONG_TO_SHORT: dict[str, str] = {
'Monday': 'Mon',
'Tuesday': 'Tue',
'Wednesay': 'Wed',
'Thursday': 'Thu',
'Friday': 'Fri',
'Saturday': 'Sat',
'Sunday': 'Sun',
}
DAYS_OF_WEEK_ISO: dict[int, str] = {
1: 'Mon', 2: 'Tue', 3: 'Wed', 4: 'Thu', 5: 'Fri', 6: 'Sat', 7: 'Sun'
}
DAYS_OF_WEEK_ISO_REVERSED: dict[str, int] = {value: key for key, value in DAYS_OF_WEEK_ISO.items()}
from warnings import deprecated
from zoneinfo import ZoneInfo
from corelibs_datetime import datetime_helpers
@deprecated("Use corelibs_datetime.datetime_helpers.create_time instead")
def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -> str:
"""
just takes a timestamp and prints out humand readable format
@@ -35,21 +22,17 @@ def create_time(timestamp: float, timestamp_format: str = "%Y-%m-%d %H:%M:%S") -
Returns:
str -- _description_
"""
return time_t.strftime(timestamp_format, time_t.localtime(timestamp))
return datetime_helpers.create_time(timestamp, timestamp_format)
@deprecated("Use corelibs_datetime.datetime_helpers.get_system_timezone instead")
def get_system_timezone():
"""Get system timezone using datetime's automatic detection"""
# Get current time with system timezone
local_time = datetime.now().astimezone()
# Extract timezone info
system_tz = local_time.tzinfo
timezone_name = str(system_tz)
return system_tz, timezone_name
return datetime_helpers.get_system_timezone()
@deprecated("Use corelibs_datetime.datetime_helpers.parse_timezone_data instead")
def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
"""
parses a string to get the ZoneInfo
@@ -62,40 +45,10 @@ def parse_timezone_data(timezone_tz: str = '') -> ZoneInfo:
Returns:
ZoneInfo -- _description_
"""
try:
return ZoneInfo(timezone_tz)
except (ZoneInfoNotFoundError, ValueError, TypeError):
# use default
time_tz, time_tz_str = get_system_timezone()
if time_tz is None:
return ZoneInfo('UTC')
# TODO build proper TZ lookup
tz_mapping = {
'JST': 'Asia/Tokyo',
'KST': 'Asia/Seoul',
'IST': 'Asia/Kolkata',
'CST': 'Asia/Shanghai', # Default to China for CST
'AEST': 'Australia/Sydney',
'AWST': 'Australia/Perth',
'EST': 'America/New_York',
'EDT': 'America/New_York',
'CDT': 'America/Chicago',
'MST': 'America/Denver',
'MDT': 'America/Denver',
'PST': 'America/Los_Angeles',
'PDT': 'America/Los_Angeles',
'GMT': 'UTC',
'UTC': 'UTC',
'CET': 'Europe/Berlin',
'CEST': 'Europe/Berlin',
'BST': 'Europe/London',
}
try:
return ZoneInfo(tz_mapping[time_tz_str])
except (ZoneInfoNotFoundError, IndexError) as e:
raise ValueError(f"No mapping for {time_tz_str}: {e}") from e
return datetime_helpers.parse_timezone_data(timezone_tz)
@deprecated("Use corelibs_datetime.datetime_helpers.get_datetime_iso8601 instead")
def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', timespec: str = 'microseconds') -> str:
"""
set a datetime in the iso8601 format with microseconds
@@ -103,12 +56,13 @@ def get_datetime_iso8601(timezone_tz: str | ZoneInfo = '', sep: str = 'T', times
Returns:
str -- _description_
"""
# parse if this is a string
if isinstance(timezone_tz, str):
timezone_tz = parse_timezone_data(timezone_tz)
return datetime.now(timezone_tz).isoformat(sep=sep, timespec=timespec)
try:
return datetime_helpers.get_datetime_iso8601(timezone_tz, sep, timespec)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.validate_date instead")
def validate_date(date: str, not_before: datetime | None = None, not_after: datetime | None = None) -> bool:
"""
check if Y-m-d or Y/m/d are parsable and valid
@@ -119,20 +73,10 @@ def validate_date(date: str, not_before: datetime | None = None, not_after: date
Returns:
bool -- _description_
"""
formats = ['%Y-%m-%d', '%Y/%m/%d']
for __format in formats:
try:
__date = datetime.strptime(date, __format).date()
if not_before is not None and __date < not_before.date():
return False
if not_after is not None and __date > not_after.date():
return False
return True
except ValueError:
continue
return False
return datetime_helpers.validate_date(date, not_before, not_after)
@deprecated("Use corelibs_datetime.datetime_helpers.parse_flexible_date instead")
def parse_flexible_date(
date_str: str,
timezone_tz: str | ZoneInfo | None = None,
@@ -154,45 +98,14 @@ def parse_flexible_date(
Returns:
datetime | None -- _description_
"""
date_str = date_str.strip()
# Try different parsing methods
parsers: list[Callable[[str], datetime]] = [
# ISO 8601 format
lambda x: datetime.fromisoformat(x), # pylint: disable=W0108
# Simple date format
lambda x: datetime.strptime(x, "%Y-%m-%d"),
# Alternative ISO formats (fallback)
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S"),
lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%f"),
]
if timezone_tz is not None:
if isinstance(timezone_tz, str):
timezone_tz = parse_timezone_data(timezone_tz)
date_new = None
for parser in parsers:
try:
date_new = parser(date_str)
break
except ValueError:
continue
if date_new is not None:
if timezone_tz is not None:
# shift time zone (default), this will change the date
# if the date has no +HH:MM it will take the local time zone as base
if shift_time_zone:
return date_new.astimezone(timezone_tz)
# just add the time zone
return date_new.replace(tzinfo=timezone_tz)
return date_new
return None
return datetime_helpers.parse_flexible_date(
date_str,
timezone_tz,
shift_time_zone
)
@deprecated("Use corelibs_datetime.datetime_helpers.compare_dates instead")
def compare_dates(date1_str: str, date2_str: str) -> None | bool:
"""
compare two dates, if the first one is newer than the second one return True
@@ -206,23 +119,10 @@ def compare_dates(date1_str: str, date2_str: str) -> None | bool:
Returns:
None | bool -- _description_
"""
try:
# Parse both dates
date1 = parse_flexible_date(date1_str)
date2 = parse_flexible_date(date2_str)
# Check if parsing was successful
if date1 is None or date2 is None:
return None
# Compare dates
return date1.date() > date2.date()
except ValueError:
return None
return datetime_helpers.compare_dates(date1_str, date2_str)
@deprecated("Use corelibs_datetime.datetime_helpers.find_newest_datetime_in_list instead")
def find_newest_datetime_in_list(date_list: list[str]) -> None | str:
"""
Find the newest date from a list of ISO 8601 formatted date strings.
@@ -234,31 +134,10 @@ def find_newest_datetime_in_list(date_list: list[str]) -> None | str:
Returns:
str: The date string with the newest/latest date, or None if list is empty or all dates are invalid
"""
if not date_list:
return None
valid_dates: list[tuple[str, datetime]] = []
for date_str in date_list:
try:
# Parse the date string and store both original string and parsed datetime
parsed_date = parse_flexible_date(date_str)
if parsed_date is None:
continue
valid_dates.append((date_str, parsed_date))
except ValueError:
# Skip invalid date strings
continue
if not valid_dates:
return None
# Find the date string with the maximum datetime value
newest_date_str: str = max(valid_dates, key=lambda x: x[1])[0]
return newest_date_str
return datetime_helpers.find_newest_datetime_in_list(date_list)
@deprecated("Use corelibs_datetime.datetime_helpers.parse_day_of_week_range instead")
def parse_day_of_week_range(dow_days: str) -> list[tuple[int, str]]:
"""
Parse a day of week list/range string and return a list of tuples with day index and name.
@@ -275,59 +154,13 @@ def parse_day_of_week_range(dow_days: str) -> list[tuple[int, str]]:
"""
# we have Sun twice because it can be 0 or 7
# Mon is 1 and Sun is 7, which is ISO standard
dow_day = dow_days.split(",")
dow_day = [day.strip() for day in dow_day if day.strip()]
__out_dow_days: list[tuple[int, str]] = []
for __dow_day in dow_day:
# if we have a "-" in there fill
if "-" in __dow_day:
__dow_range = __dow_day.split("-")
__dow_range = [day.strip().capitalize() for day in __dow_range if day.strip()]
try:
start_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[0]]
end_day = DAYS_OF_WEEK_ISO_REVERSED[__dow_range[1]]
except KeyError:
# try long time
try:
start_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[0]]]
end_day = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[__dow_range[1]]]
except KeyError as e:
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
# Check if this spans across the weekend (e.g., Fri-Mon)
if start_day > end_day:
# Handle weekend-spanning range: start_day to 7, then 1 to end_day
__out_dow_days.extend(
[
(i, DAYS_OF_WEEK_ISO[i])
for i in range(start_day, 8) # start_day to Sunday (7)
]
)
__out_dow_days.extend(
[
(i, DAYS_OF_WEEK_ISO[i])
for i in range(1, end_day + 1) # Monday (1) to end_day
]
)
else:
# Normal range: start_day to end_day
__out_dow_days.extend(
[
(i, DAYS_OF_WEEK_ISO[i])
for i in range(start_day, end_day + 1)
]
)
else:
try:
__out_dow_days.append((DAYS_OF_WEEK_ISO_REVERSED[__dow_day], __dow_day))
except KeyError as e:
raise ValueError(f"Invalid day of week entry found: {__dow_day}: {e}") from e
# if there are duplicates, alert
if len(__out_dow_days) != len(set(__out_dow_days)):
raise ValueError(f"Duplicate day of week entries found: {__out_dow_days}")
return __out_dow_days
try:
return datetime_helpers.parse_day_of_week_range(dow_days)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.parse_time_range instead")
def parse_time_range(time_str: str, time_format: str = "%H:%M") -> tuple[time, time]:
"""
Parse a time range string in the format "HH:MM-HH:MM" and return a tuple of two time objects.
@@ -343,22 +176,13 @@ def parse_time_range(time_str: str, time_format: str = "%H:%M") -> tuple[time, t
Returns:
tuple[time, time] -- start time, end time: leading zeros formattd
"""
__time_str = time_str.strip()
# split by "-"
__time_split = __time_str.split("-")
if len(__time_split) != 2:
raise ValueError(f"Invalid time block: {__time_str}")
try:
__time_start = datetime.strptime(__time_split[0], time_format).time()
__time_end = datetime.strptime(__time_split[1], time_format).time()
except ValueError as e:
raise ValueError(f"Invalid time block format [{__time_str}]: {e}") from e
if __time_start >= __time_end:
raise ValueError(f"Invalid time block set, start time after end time or equal: {__time_str}")
return __time_start, __time_end
return datetime_helpers.parse_time_range(time_str, time_format)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
@deprecated("Use corelibs_datetime.datetime_helpers.times_overlap_or_connect instead")
def times_overlap_or_connect(time1: tuple[time, time], time2: tuple[time, time], allow_touching: bool = False) -> bool:
"""
Check if two time ranges overlap or connect
@@ -371,16 +195,10 @@ def times_overlap_or_connect(time1: tuple[time, time], time2: tuple[time, time],
Returns:
bool: True if ranges overlap or connect (based on allow_touching)
"""
start1, end1 = time1
start2, end2 = time2
if allow_touching:
# Only check for actual overlap (touching is OK)
return start1 < end2 and start2 < end1
# Check for overlap OR touching
return start1 <= end2 and start2 <= end1
return datetime_helpers.times_overlap_or_connect(time1, time2, allow_touching)
@deprecated("Use corelibs_datetime.datetime_helpers.is_time_in_range instead")
def is_time_in_range(current_time: str, start_time: str, end_time: str) -> bool:
"""
Check if current_time is within start_time and end_time (inclusive)
@@ -395,18 +213,10 @@ def is_time_in_range(current_time: str, start_time: str, end_time: str) -> bool:
bool -- _description_
"""
# Convert string times to time objects
current = datetime.strptime(current_time, "%H:%M:%S").time()
start = datetime.strptime(start_time, "%H:%M:%S").time()
end = datetime.strptime(end_time, "%H:%M:%S").time()
# Handle case where range crosses midnight (e.g., 22:00 to 06:00)
if start <= end:
# Normal case: start time is before end time
return start <= current <= end
# Crosses midnight: e.g., 22:00 to 06:00
return current >= start or current <= end
return datetime_helpers.is_time_in_range(current_time, start_time, end_time)
@deprecated("Use corelibs_datetime.datetime_helpers.reorder_weekdays_from_today instead")
def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
"""
Reorder the days of the week starting from the specified base_day.
@@ -418,18 +228,8 @@ def reorder_weekdays_from_today(base_day: str) -> dict[int, str]:
dict[int, str] -- A dictionary mapping day numbers to day names.
"""
try:
today_num = DAYS_OF_WEEK_ISO_REVERSED[base_day]
except KeyError:
try:
today_num = DAYS_OF_WEEK_ISO_REVERSED[DAYS_OF_WEEK_LONG_TO_SHORT[base_day]]
except KeyError as e:
raise ValueError(f"Invalid day name provided: {base_day}: {e}") from e
# Convert to list of tuples
items = list(DAYS_OF_WEEK_ISO.items())
# Reorder: from today onwards + from beginning to yesterday
reordered_items = items[today_num - 1:] + items[:today_num - 1]
# Convert back to dictionary
return dict(reordered_items)
return datetime_helpers.reorder_weekdays_from_today(base_day)
except KeyError as e:
raise ValueError(f"Deprecated ValueError, change to KeyError: {e}") from e
# __END__

View File

@@ -2,19 +2,22 @@
Convert timestamp strings with time units into seconds and vice versa.
"""
from math import floor
import re
from corelibs.var_handling.var_helpers import is_float
from warnings import deprecated
from corelibs_datetime import timestamp_convert
from corelibs_datetime.timestamp_convert import TimeParseError as NewTimeParseError, TimeUnitError as NewTimeUnitError
@deprecated("Use corelibs_datetime.timestamp_convert.TimeParseError instead")
class TimeParseError(Exception):
"""Custom exception for time parsing errors."""
@deprecated("Use corelibs_datetime.timestamp_convert.TimeUnitError instead")
class TimeUnitError(Exception):
"""Custom exception for time parsing errors."""
@deprecated("Use corelibs_datetime.timestamp_convert.convert_to_seconds instead")
def convert_to_seconds(time_string: str | int | float) -> int:
"""
Conver a string with time units into a seconds string
@@ -35,67 +38,15 @@ def convert_to_seconds(time_string: str | int | float) -> int:
# skip out if this is a number of any type
# numbers will br made float, rounded and then converted to int
if is_float(time_string):
return int(round(float(time_string)))
time_string = str(time_string)
# Check if the time string is negative
negative = time_string.startswith('-')
if negative:
time_string = time_string[1:] # Remove the negative sign for processing
# Define time unit conversion factors
unit_factors: dict[str, int] = {
'Y': 31536000, # 365 days * 86400 seconds/day
'M': 2592000 * 12, # 1 year in seconds (assuming 365 days per year)
'd': 86400, # 1 day in seconds
'h': 3600, # 1 hour in seconds
'm': 60, # minutes to seconds
's': 1 # 1 second in seconds
}
long_unit_names: dict[str, str] = {
'year': 'Y',
'years': 'Y',
'month': 'M',
'months': 'M',
'day': 'd',
'days': 'd',
'hour': 'h',
'hours': 'h',
'minute': 'm',
'minutes': 'm',
'min': 'm',
'second': 's',
'seconds': 's',
'sec': 's',
}
total_seconds = 0
seen_units: list[str] = [] # Track units that have been encountered
# Use regex to match number and time unit pairs
for match in re.finditer(r'(\d+)\s*([a-zA-Z]+)', time_string):
value, unit = int(match.group(1)), match.group(2)
# full name check, fallback to original name
unit = long_unit_names.get(unit.lower(), unit)
# Check for duplicate units
if unit in seen_units:
raise TimeParseError(f"Unit '{unit}' appears more than once.")
# Check invalid unit
if unit not in unit_factors:
raise TimeUnitError(f"Unit '{unit}' is not a valid unit name.")
# Add to total seconds based on the units
if unit in unit_factors:
total_seconds += value * unit_factors[unit]
seen_units.append(unit)
return -total_seconds if negative else total_seconds
try:
return timestamp_convert.convert_to_seconds(time_string)
except NewTimeParseError as e:
raise TimeParseError(f"Deprecated, use corelibs_datetime.timestamp_convert.TimeParseError: {e}") from e
except NewTimeUnitError as e:
raise TimeUnitError(f"Deprecated, use corelibs_datetime.timestamp_convert.TimeUnitError: {e}") from e
@deprecated("Use corelibs_datetime.timestamp_convert.seconds_to_string instead")
def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str:
"""
Convert seconds to compact human readable format (e.g., "1d 2h 3m 4.567s")
@@ -111,46 +62,10 @@ def seconds_to_string(seconds: str | int | float, show_microseconds: bool = Fals
Returns:
str: Compact human readable time format
"""
# if not int or float, return as is
if not isinstance(seconds, (int, float)):
return seconds
# Handle negative values
negative = seconds < 0
seconds = abs(seconds)
whole_seconds = int(seconds)
fractional = seconds - whole_seconds
days = whole_seconds // 86400
hours = (whole_seconds % 86400) // 3600
minutes = (whole_seconds % 3600) // 60
secs = whole_seconds % 60
parts: list[str] = []
if days > 0:
parts.append(f"{days}d")
if hours > 0:
parts.append(f"{hours}h")
if minutes > 0:
parts.append(f"{minutes}m")
# Handle seconds with fractional part
if fractional > 0:
if show_microseconds:
total_seconds = secs + fractional
formatted = f"{total_seconds:.6f}".rstrip('0').rstrip('.')
parts.append(f"{formatted}s")
else:
total_seconds = secs + fractional
formatted = f"{total_seconds:.3f}".rstrip('0').rstrip('.')
parts.append(f"{formatted}s")
elif secs > 0 or not parts:
parts.append(f"{secs}s")
result = " ".join(parts)
return f"-{result}" if negative else result
return timestamp_convert.seconds_to_string(seconds, show_microseconds)
@deprecated("Use corelibs_datetime.timestamp_convert.convert_timestamp instead")
def convert_timestamp(timestamp: float | int | str, show_microseconds: bool = True) -> str:
"""
format timestamp into human readable format. This function will add 0 values between set values
@@ -168,33 +83,6 @@ def convert_timestamp(timestamp: float | int | str, show_microseconds: bool = Tr
Returns:
str -- _description_
"""
if not isinstance(timestamp, (int, float)):
return timestamp
# cut of the ms, but first round them up to four
__timestamp_ms_split = str(round(timestamp, 4)).split(".")
timestamp = int(__timestamp_ms_split[0])
negative = timestamp < 0
timestamp = abs(timestamp)
try:
ms = int(__timestamp_ms_split[1])
except IndexError:
ms = 0
timegroups = (86400, 3600, 60, 1)
output: list[int] = []
for i in timegroups:
output.append(int(floor(timestamp / i)))
timestamp = timestamp % i
# output has days|hours|min|sec ms
time_string = ""
if output[0]:
time_string = f"{output[0]}d "
if output[0] or output[1]:
time_string += f"{output[1]}h "
if output[0] or output[1] or output[2]:
time_string += f"{output[2]}m "
time_string += f"{output[3]}s"
if show_microseconds:
time_string += f" {ms}ms" if ms else " 0ms"
return f"-{time_string}" if negative else time_string
return timestamp_convert.convert_timestamp(timestamp, show_microseconds)
# __END__

View File

@@ -2,31 +2,20 @@
Current timestamp strings and time zones
"""
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from warnings import deprecated
from zoneinfo import ZoneInfo
from corelibs_datetime import timestamp_strings
class TimestampStrings:
class TimestampStrings(timestamp_strings.TimestampStrings):
"""
set default time stamps
"""
TIME_ZONE: str = 'Asia/Tokyo'
@deprecated("Use corelibs_datetime.timestamp_strings.TimestampStrings instead")
def __init__(self, time_zone: str | ZoneInfo | None = None):
self.timestamp_now = datetime.now()
# set time zone as string
time_zone = time_zone if time_zone is not None else self.TIME_ZONE
self.time_zone = str(time_zone) if not isinstance(time_zone, str) else time_zone
# set ZoneInfo type
try:
self.time_zone_zi = ZoneInfo(self.time_zone)
except ZoneInfoNotFoundError as e:
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
self.timestamp_now_tz = datetime.now(self.time_zone_zi)
self.today = self.timestamp_now.strftime('%Y-%m-%d')
self.timestamp = self.timestamp_now.strftime("%Y-%m-%d %H:%M:%S")
self.timestamp_tz = self.timestamp_now_tz.strftime("%Y-%m-%d %H:%M:%S %Z")
self.timestamp_file = self.timestamp_now.strftime("%Y-%m-%d_%H%M%S")
super().__init__(time_zone)
# __END__

View File

@@ -0,0 +1,76 @@
"""
Main SQL base for any SQL calls
This is a wrapper for SQLiteIO or other future DB Interfaces
[Note: at the moment only SQLiteIO is implemented]
- on class creation connection with ValueError on fail
- connect method checks if already connected and warns
- connection class fails with ValueError if not valid target is selected (SQL wrapper type)
- connected check class method
- a process class that returns data as list or False if end or error
TODO: adapt more CoreLibs DB IO class flow here
"""
from typing import TYPE_CHECKING, Any, Literal
from corelibs.debug_handling.debug_helpers import call_stack
from corelibs.db_handling.sqlite_io import SQLiteIO
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger
IDENT_SPLIT_CHARACTER: str = ':'
class SQLMain:
"""Main SQL interface class"""
def __init__(self, log: 'Logger', db_ident: str):
self.log = log
self.dbh: SQLiteIO | None = None
self.db_target: str | None = None
self.connect(db_ident)
if not self.connected():
raise ValueError(f'Failed to connect to database [{call_stack()}]')
def connect(self, db_ident: str):
"""setup basic connection"""
if self.dbh is not None and self.dbh.conn is not None:
self.log.warning(f"A database connection already exists for: {self.db_target} [{call_stack()}]")
return
self.db_target, db_dsn = db_ident.split(IDENT_SPLIT_CHARACTER)
match self.db_target:
case 'sqlite':
# this is a Path only at the moment
self.dbh = SQLiteIO(self.log, db_dsn, row_factory='Dict')
case _:
raise ValueError(f'SQL interface for {self.db_target} is not implemented [{call_stack()}]')
if not self.dbh.db_connected():
raise ValueError(f"DB Connection failed for: {self.db_target} [{call_stack()}]")
def close(self):
"""close connection"""
if self.dbh is None or not self.connected():
return
# self.log.info(f"Close DB Connection: {self.db_target} [{call_stack()}]")
self.dbh.db_close()
def connected(self) -> bool:
"""check connectuon"""
if self.dbh is None or not self.dbh.db_connected():
self.log.warning(f"No connection [{call_stack()}]")
return False
return True
def process_query(
self, query: str, params: tuple[Any, ...] | None = None
) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
"""mini wrapper for execute query"""
if self.dbh is not None:
result = self.dbh.execute_query(query, params)
if result is False:
return False
else:
self.log.error(f"Problem connecting to db: {self.db_target} [{call_stack()}]")
return False
return result
# __END__

View File

View File

@@ -0,0 +1,219 @@
"""
Send email wrapper
"""
import smtplib
from email.message import EmailMessage
from email.header import Header
from email.utils import formataddr, parseaddr
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger
class SendEmail:
"""
send emails based on a template to a list of receivers
"""
def __init__(
self,
log: "Logger",
settings: dict[str, Any],
template: dict[str, str],
from_email: str,
combined_send: bool = True,
receivers: list[str] | None = None,
data: list[dict[str, str]] | None = None,
):
"""
init send email class
Args:
template (dict): Dictionary with body and subject
from_email (str): from email as "Name" <email>
combined_send (bool): True for sending as one set for all receivers
receivers (list): list of emails to send to
data (dict): data to replace in template
args (Namespace): _description_
"""
self.log = log
self.settings = settings
# internal settings
self.template = template
self.from_email = from_email
self.combined_send = combined_send
self.receivers = receivers
self.data = data
def send_email(
self,
data: list[dict[str, str]] | None,
receivers: list[str] | None,
template: dict[str, str] | None = None,
from_email: str | None = None,
combined_send: bool | None = None,
test_only: bool | None = None
):
"""
build email and send
Arguments:
data {list[dict[str, str]] | None} -- _description_
receivers {list[str] | None} -- _description_
combined_send {bool | None} -- _description_
Keyword Arguments:
template {dict[str, str] | None} -- _description_ (default: {None})
from_email {str | None} -- _description_ (default: {None})
Raises:
ValueError: _description_
ValueError: _description_
"""
if data is None and self.data is not None:
data = self.data
if data is None:
raise ValueError("No replace data set, cannot send email")
if receivers is None and self.receivers is not None:
receivers = self.receivers
if receivers is None:
raise ValueError("No receivers list set, cannot send email")
if combined_send is None:
combined_send = self.combined_send
if test_only is not None:
self.settings['test'] = test_only
if template is None:
template = self.template
if from_email is None:
from_email = self.from_email
if not template['subject'] or not template['body']:
raise ValueError("Both Subject and Body must be set")
self.log.debug(
"[EMAIL]:\n"
f"Subject: {template['subject']}\n"
f"Body: {template['body']}\n"
f"From: {from_email}\n"
f"Combined send: {combined_send}\n"
f"Receivers: {receivers}\n"
f"Replace data: {data}"
)
# send email
self.send_email_list(
self.prepare_email_content(
from_email, template, data
),
receivers,
combined_send,
test_only
)
def prepare_email_content(
self,
from_email: str,
template: dict[str, str],
data: list[dict[str, str]],
) -> list[EmailMessage]:
"""
prepare email for sending
Args:
template (dict): template data for this email
data (dict): data to replace in email
Returns:
list: Email Message Objects as list
"""
_subject = ""
_body = ""
msg: list[EmailMessage] = []
for replace in data:
_subject = template["subject"]
_body = template["body"]
for key, value in replace.items():
placeholder = f"{{{{{key}}}}}"
_subject = _subject.replace(placeholder, value)
_body = _body.replace(placeholder, value)
name, addr = parseaddr(from_email)
if name:
# Encode the name part with MIME encoding
encoded_name = str(Header(name, 'utf-8'))
from_email_encoded = formataddr((encoded_name, addr))
else:
from_email_encoded = from_email
# create a simple email and add subhect, from email
msg_email = EmailMessage()
# msg.set_content(_body, charset='utf-8', cte='quoted-printable')
msg_email.set_content(_body, charset="utf-8")
msg_email["Subject"] = _subject
msg_email["From"] = from_email_encoded
# push to array for sening
msg.append(msg_email)
return msg
def send_email_list(
self,
emails: list[EmailMessage],
receivers: list[str],
combined_send: bool | None = None,
test_only: bool | None = None
):
"""
send email to receivers list
Args:
email (list): Email Message object with set obdy, subject, from as list
receivers (array): email receivers list as array
combined_send (bool): True for sending as one set for all receivers
"""
if test_only is not None:
self.settings['test'] = test_only
# localhost (postfix does the rest)
smtp = None
smtp_host = self.settings.get('smtp_host', "localhost")
try:
smtp = smtplib.SMTP(smtp_host)
except ConnectionRefusedError as e:
self.log.error("Could not open SMTP connection to: %s, %s", smtp_host, e)
# prepare receiver list
receivers_encoded: list[str] = []
for __receiver in receivers:
to_name, to_addr = parseaddr(__receiver)
if to_name:
# Encode the name part with MIME encoding
encoded_to_name = str(Header(to_name, 'utf-8'))
receivers_encoded.append(formataddr((encoded_to_name, to_addr)))
else:
receivers_encoded.append(__receiver)
# loop over messages and then over recievers
for msg in emails:
if combined_send is True:
msg["To"] = ", ".join(receivers_encoded)
if not self.settings.get('test'):
if smtp is not None:
smtp.send_message(msg, msg["From"], receivers_encoded)
else:
self.log.info(f"[EMAIL] Test, not sending email\n{msg}")
else:
for receiver in receivers_encoded:
self.log.debug(f"===> Send to: {receiver}")
if "To" in msg:
msg.replace_header("To", receiver)
else:
msg["To"] = receiver
if not self.settings.get('test'):
if smtp is not None:
smtp.send_message(msg)
else:
self.log.info(f"[EMAIL] Test, not sending email\n{msg}")
# close smtp
if smtp is not None:
smtp.quit()
# __END__

View File

@@ -0,0 +1,75 @@
"""
File check if BOM encoded, needed for CSV load
"""
from pathlib import Path
from typing import TypedDict
class BomEncodingInfo(TypedDict):
"""BOM encoding info"""
has_bom: bool
bom_type: str | None
encoding: str | None
bom_length: int
bom_pattern: bytes | None
def is_bom_encoded(file_path: Path) -> bool:
"""
Detect if a file is BOM encoded
Args:
file_path (str): Path to the file to check
Returns:
bool: True if file has BOM, False otherwise
"""
return is_bom_encoded_info(file_path)['has_bom']
def is_bom_encoded_info(file_path: Path) -> BomEncodingInfo:
"""
Enhanced BOM detection with additional file analysis
Args:
file_path (str): Path to the file to check
Returns:
dict: Comprehensive BOM and encoding information
"""
try:
# Read first 1024 bytes for analysis
with open(file_path, 'rb') as f:
header = f.read(4)
bom_patterns = {
b'\xef\xbb\xbf': ('UTF-8', 'utf-8', 3),
b'\xff\xfe\x00\x00': ('UTF-32 LE', 'utf-32-le', 4),
b'\x00\x00\xfe\xff': ('UTF-32 BE', 'utf-32-be', 4),
b'\xff\xfe': ('UTF-16 LE', 'utf-16-le', 2),
b'\xfe\xff': ('UTF-16 BE', 'utf-16-be', 2),
}
for bom_pattern, (encoding_name, encoding, length) in bom_patterns.items():
if header.startswith(bom_pattern):
return {
'has_bom': True,
'bom_type': encoding_name,
'encoding': encoding,
'bom_length': length,
'bom_pattern': bom_pattern
}
return {
'has_bom': False,
'bom_type': None,
'encoding': None,
'bom_length': 0,
'bom_pattern': None
}
except Exception as e:
raise ValueError(f"Error checking BOM encoding: {e}") from e
# __END__

View File

@@ -4,11 +4,38 @@ Various dictionary, object and list hashers
import json
import hashlib
from typing import Any
from typing import Any, cast, Sequence
def hash_object(obj: Any) -> str:
"""
RECOMMENDED for new use
Create a hash for any dict or list with mixed key types
Arguments:
obj {Any} -- _description_
Returns:
str -- _description_
"""
def normalize(o: Any) -> Any:
if isinstance(o, dict):
# Sort by repr of keys to handle mixed types (str, int, etc.)
o = cast(dict[Any, Any], o)
return tuple(sorted((repr(k), normalize(v)) for k, v in o.items()))
if isinstance(o, (list, tuple)):
o = cast(Sequence[Any], o)
return tuple(normalize(item) for item in o)
return repr(o)
normalized = normalize(obj)
return hashlib.sha256(str(normalized).encode()).hexdigest()
def dict_hash_frozen(data: dict[Any, Any]) -> int:
"""
NOT RECOMMENDED, use dict_hash_crc or hash_object instead
If used, DO NOT CHANGE
hash a dict via freeze
Args:
@@ -22,18 +49,25 @@ def dict_hash_frozen(data: dict[Any, Any]) -> int:
def dict_hash_crc(data: dict[Any, Any] | list[Any]) -> str:
"""
Create a sha256 hash over dict
LEGACY METHOD, must be kept for fallback, if used by other code, DO NOT CHANGE
Create a sha256 hash over dict or list
alternative for
dict_hash_frozen
Args:
data (dict | list): _description_
data (dict[Any, Any] | list[Any]): _description_
Returns:
str: _description_
str: sha256 hash, prefiex with HO_ if fallback used
"""
return hashlib.sha256(
json.dumps(data, sort_keys=True, ensure_ascii=True).encode('utf-8')
).hexdigest()
try:
return hashlib.sha256(
# IT IS IMPORTANT THAT THE BELOW CALL STAYS THE SAME AND DOES NOT CHANGE OR WE WILL GET DIFFERENT HASHES
# separators=(',', ':') to get rid of spaces, but if this is used the hash will be different, DO NOT ADD
json.dumps(data, sort_keys=True, ensure_ascii=True, default=str).encode('utf-8')
).hexdigest()
except TypeError:
# Fallback tod different hasher, will return DIFFERENT hash than above, so only usable in int/str key mixes
return "HO_" + hash_object(data)
# __END__

View File

@@ -2,6 +2,7 @@
List type helpers
"""
import json
from typing import Any, Sequence
@@ -44,4 +45,31 @@ def is_list_in_list(
# Get the difference and extract just the values
return [item for item, _ in set_a - set_b]
def make_unique_list_of_dicts(dict_list: list[Any]) -> list[Any]:
"""
Create a list of unique dictionary entries
Arguments:
dict_list {list[Any]} -- _description_
Returns:
list[Any] -- _description_
"""
try:
# try json dumps, can fail with int and str index types
return list(
{
json.dumps(d, sort_keys=True, ensure_ascii=True, separators=(',', ':')): d
for d in dict_list
}.values()
)
except TypeError:
# Fallback for non-serializable entries, slow but works
unique: list[Any] = []
for d in dict_list:
if d not in unique:
unique.append(d)
return unique
# __END__

View File

@@ -11,15 +11,68 @@ from datetime import datetime
import time
from pathlib import Path
import atexit
from enum import Flag, auto
from typing import MutableMapping, TextIO, TypedDict, Any, TYPE_CHECKING, cast
from corelibs_text_colors.text_colors import Colors
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
from corelibs.string_handling.text_colors import Colors
from corelibs.debug_handling.debug_helpers import call_stack, exception_stack
if TYPE_CHECKING:
from multiprocessing import Queue
class ConsoleFormat(Flag):
"""console format type bitmap flags"""
TIME = auto()
TIME_SECONDS = auto()
TIME_MILLISECONDS = auto()
TIME_MICROSECONDS = auto()
TIMEZONE = auto()
NAME = auto()
FILE = auto()
FUNCTION = auto()
LINENO = auto()
LEVEL = auto()
class ConsoleFormatSettings:
"""Console format quick settings groups"""
# shows everything, time with milliseconds, and time zone, log name, file, function, line number
ALL = (
ConsoleFormat.TIME |
ConsoleFormat.TIMEZONE |
ConsoleFormat.NAME |
ConsoleFormat.FILE |
ConsoleFormat.FUNCTION |
ConsoleFormat.LINENO |
ConsoleFormat.LEVEL
)
# show time with no time zone, file, line and level
CONDENSED = ConsoleFormat.TIME | ConsoleFormat.FILE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# only time and level
MINIMAL = ConsoleFormat.TIME | ConsoleFormat.LEVEL
# only level
BARE = ConsoleFormat.LEVEL
# only message
NONE = ConsoleFormat(0)
@staticmethod
def from_string(setting_str: str, default: ConsoleFormat | None = None) -> ConsoleFormat | None:
"""
Get a console format setting, if does not exist set to None
Arguments:
setting_str {str} -- what to search for
default {ConsoleFormat | None} -- if not found return this (default: {None})
Returns:
ConsoleFormat | None -- found ConsoleFormat or None
"""
if hasattr(ConsoleFormatSettings, setting_str):
return getattr(ConsoleFormatSettings, setting_str)
return default
# MARK: Log settings TypedDict
class LogSettings(TypedDict):
"""log settings, for Log setup"""
@@ -28,6 +81,7 @@ class LogSettings(TypedDict):
per_run_log: bool
console_enabled: bool
console_color_output_enabled: bool
console_format_type: ConsoleFormat
add_start_info: bool
add_end_info: bool
log_queue: 'Queue[str] | None'
@@ -338,6 +392,24 @@ class LogParent:
except IndexError:
return LoggingLevel.NOTSET
def any_handler_is_minimum_level(self, log_level: LoggingLevel) -> bool:
"""
if any handler is set to minimum level
Arguments:
log_level {LoggingLevel} -- _description_
Returns:
bool -- _description_
"""
for handler in self.handlers.values():
try:
if LoggingLevel.from_any(handler.level).includes(log_level):
return True
except (IndexError, AttributeError):
continue
return False
@staticmethod
def validate_log_level(log_level: Any) -> bool:
"""
@@ -395,6 +467,9 @@ class Log(LogParent):
logger setup
"""
CONSOLE_HANDLER: str = 'stream_handler'
FILE_HANDLER: str = 'file_handler'
# spacer lenght characters and the character
SPACER_CHAR: str = '='
SPACER_LENGTH: int = 32
@@ -409,6 +484,8 @@ class Log(LogParent):
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
# do not print log title, file, function and line number
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": True,
"add_end_info": False,
"log_queue": None,
@@ -419,7 +496,10 @@ class Log(LogParent):
self,
log_path: Path,
log_name: str,
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str]'] | LogSettings | None = None,
log_settings: (
dict[str, 'LoggingLevel | str | bool | None | Queue[str] | ConsoleFormat'] | # noqa: E501 # pylint: disable=line-too-long
LogSettings | None
) = None,
other_handlers: dict[str, Any] | None = None
):
LogParent.__init__(self)
@@ -455,14 +535,16 @@ class Log(LogParent):
# in the file writer too, for the ones where color is set BEFORE the format
# Any is logging.StreamHandler, logging.FileHandler and all logging.handlers.*
self.handlers: dict[str, Any] = {}
self.add_handler('file_handler', self.__create_file_handler(
'file_handler', self.log_settings['log_level_file'], log_path)
self.add_handler(self.FILE_HANDLER, self.__create_file_handler(
self.FILE_HANDLER, self.log_settings['log_level_file'], log_path)
)
if self.log_settings['console_enabled']:
# console
self.add_handler('stream_handler', self.__create_console_handler(
'stream_handler', self.log_settings['log_level_console'])
)
self.add_handler(self.CONSOLE_HANDLER, self.__create_console_handler(
self.CONSOLE_HANDLER,
self.log_settings['log_level_console'],
console_format_type=self.log_settings['console_format_type'],
))
# add other handlers,
if other_handlers is not None:
for handler_key, handler in other_handlers.items():
@@ -488,7 +570,8 @@ class Log(LogParent):
# MARK: parse log settings
def __parse_log_settings(
self,
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str]'] | LogSettings | None
log_settings: dict[str, 'LoggingLevel | str | bool | None | Queue[str] | ConsoleFormat'] | # noqa: E501 # pylint: disable=line-too-long
LogSettings | None
) -> LogSettings:
# skip with defaul it not set
if log_settings is None:
@@ -518,6 +601,10 @@ class Log(LogParent):
if not isinstance(__setting := log_settings.get(__log_entry, ''), bool):
__setting = self.DEFAULT_LOG_SETTINGS.get(__log_entry, True)
default_log_settings[__log_entry] = __setting
# check console log type
if (console_format_type := log_settings.get('console_format_type')) is None:
console_format_type = self.DEFAULT_LOG_SETTINGS['console_format_type']
default_log_settings['console_format_type'] = cast('ConsoleFormat', console_format_type)
# check log queue
__setting = log_settings.get('log_queue', self.DEFAULT_LOG_SETTINGS['log_queue'])
if __setting is not None:
@@ -551,34 +638,182 @@ class Log(LogParent):
self.handlers[handler_name] = handler
return True
# MARK: console logger format
def __build_console_format_from_string(self, console_format_type: ConsoleFormat) -> str:
"""
Build console format string from the given console format type
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
str -- _description_
"""
format_string = ''
# time part if any of the times are requested
if (
ConsoleFormat.TIME in console_format_type or
ConsoleFormat.TIME_SECONDS in console_format_type or
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME_MICROSECONDS in console_format_type
):
format_string += '[%(asctime)s] '
# set log name
if ConsoleFormat.NAME in console_format_type:
format_string += '[%(name)s] '
# for any file/function/line number call
if (
ConsoleFormat.FILE in console_format_type or
ConsoleFormat.FUNCTION in console_format_type or
ConsoleFormat.LINENO in console_format_type
):
format_string += '['
set_group: list[str] = []
if ConsoleFormat.FILE in console_format_type:
set_group.append('%(filename)s')
if ConsoleFormat.FUNCTION in console_format_type:
set_group.append('%(funcName)s')
if ConsoleFormat.LINENO in console_format_type:
set_group.append('%(lineno)d')
format_string += ':'.join(set_group)
format_string += '] '
# level if wanted
if ConsoleFormat.LEVEL in console_format_type:
format_string += '<%(levelname)s> '
# always message
format_string += '%(message)s'
return format_string
def __set_time_format_for_console_formatter(
self, formatter_console: CustomConsoleFormatter | logging.Formatter, console_format_type: ConsoleFormat
) -> None:
"""
Format time for a given format handler, this is for console format only
Arguments:
formatter_console {CustomConsoleFormatter | logging.Formatter} -- _description_
console_format_type {ConsoleFormat} -- _description_
"""
# default for TIME is milliseconds
# if we have multiple set, the smallest precision wins
if ConsoleFormat.TIME_MICROSECONDS in console_format_type:
iso_precision = 'microseconds'
elif (
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME in console_format_type
):
iso_precision = 'milliseconds'
elif ConsoleFormat.TIME_SECONDS in console_format_type:
iso_precision = 'seconds'
else:
iso_precision = 'milliseconds'
# do timestamp modification only if we have time requested
if (
ConsoleFormat.TIME in console_format_type or
ConsoleFormat.TIME_SECONDS in console_format_type or
ConsoleFormat.TIME_MILLISECONDS in console_format_type or
ConsoleFormat.TIME_MICROSECONDS in console_format_type
):
# if we have with TZ we as the asttimezone call
if ConsoleFormat.TIMEZONE in console_format_type:
formatter_console.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.astimezone()
.isoformat(sep=" ", timespec=iso_precision)
)
else:
formatter_console.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.isoformat(sep=" ", timespec=iso_precision)
)
def __set_console_formatter(self, console_format_type: ConsoleFormat) -> CustomConsoleFormatter | logging.Formatter:
"""
Build the full formatter and return it
Arguments:
console_format_type {ConsoleFormat} -- _description_
Returns:
CustomConsoleFormatter | logging.Formatter -- _description_
"""
format_string = self.__build_console_format_from_string(console_format_type)
if self.log_settings['console_color_output_enabled']:
# formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
formatter_console = CustomConsoleFormatter(format_string)
else:
# formatter_console = logging.Formatter(format_string, datefmt=format_date)
formatter_console = logging.Formatter(format_string)
self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
self.log_settings['console_format_type'] = console_format_type
return formatter_console
# MARK: console handler update
def update_console_formatter(
self,
console_format_type: ConsoleFormat,
):
"""
Update the console formatter for format layout and time stamp format
Arguments:
console_format_type {ConsoleFormat} -- _description_
"""
# skip if console not enabled
if not self.log_settings['console_enabled']:
return
# skip if format has not changed
if self.log_settings['console_format_type'] == console_format_type:
return
# update the formatter
self.handlers[self.CONSOLE_HANDLER].setFormatter(
self.__set_console_formatter(console_format_type)
)
def get_console_formatter(self) -> ConsoleFormat:
"""
Get the current console formatter, this the settings type
Note that if eg "ALL" is set it will return the combined information but not the ALL flag name itself
Returns:
ConsoleFormat -- _description_
"""
return self.log_settings['console_format_type']
# MARK: console handler
def __create_console_handler(
self, handler_name: str,
log_level_console: LoggingLevel = LoggingLevel.WARNING, filter_exceptions: bool = True
log_level_console: LoggingLevel = LoggingLevel.WARNING,
filter_exceptions: bool = True,
console_format_type: ConsoleFormat = ConsoleFormatSettings.ALL,
) -> logging.StreamHandler[TextIO]:
# console logger
if not self.validate_log_level(log_level_console):
log_level_console = self.DEFAULT_LOG_LEVEL_CONSOLE
console_handler = logging.StreamHandler()
# format layouts
format_string = (
'[%(asctime)s.%(msecs)03d] '
'[%(name)s] '
'[%(filename)s:%(funcName)s:%(lineno)d] '
'<%(levelname)s> '
'%(message)s'
)
format_date = "%Y-%m-%d %H:%M:%S"
# color or not
if self.log_settings['console_color_output_enabled']:
formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
else:
formatter_console = logging.Formatter(format_string, datefmt=format_date)
# print(f"Console format type: {console_format_type}")
# build the format string based on what flags are set
# format_string = self.__build_console_format_from_string(console_format_type)
# # basic date, but this will be overridden to ISO in formatTime
# # format_date = "%Y-%m-%d %H:%M:%S"
# # color or not
# if self.log_settings['console_color_output_enabled']:
# # formatter_console = CustomConsoleFormatter(format_string, datefmt=format_date)
# formatter_console = CustomConsoleFormatter(format_string)
# else:
# # formatter_console = logging.Formatter(format_string, datefmt=format_date)
# formatter_console = logging.Formatter(format_string)
# # set the time format
# self.__set_time_format_for_console_formatter(formatter_console, console_format_type)
console_handler.set_name(handler_name)
console_handler.setLevel(log_level_console.name)
# do not show exceptions logs on console
console_handler.addFilter(CustomHandlerFilter('console', filter_exceptions))
console_handler.setFormatter(formatter_console)
console_handler.setFormatter(self.__set_console_formatter(console_format_type))
return console_handler
# MARK: file handler
@@ -614,13 +849,14 @@ class Log(LogParent):
formatter_file_handler = logging.Formatter(
(
# time stamp
'[%(asctime)s.%(msecs)03d] '
# '[%(asctime)s.%(msecs)03d] '
'[%(asctime)s] '
# log name
'[%(name)s] '
# filename + pid
'[%(filename)s:%(process)d] '
# path + func + line number
'[%(pathname)s:%(funcName)s:%(lineno)d] '
# '[%(filename)s:%(process)d] '
# pid + path/filename + func + line number
'[%(process)d:%(pathname)s:%(funcName)s:%(lineno)d] '
# error level
'<%(levelname)s> '
# message
@@ -628,6 +864,13 @@ class Log(LogParent):
),
datefmt="%Y-%m-%dT%H:%M:%S",
)
formatter_file_handler.formatTime = (
lambda record, datefmt=None:
datetime
.fromtimestamp(record.created)
.astimezone()
.isoformat(sep="T", timespec="microseconds")
)
file_handler.set_name(handler_name)
file_handler.setLevel(log_level_file.name)
# do not show errors flagged with console (they are from exceptions)

View File

View File

@@ -0,0 +1,35 @@
"""
Various math helpers
"""
import math
def gcd(a: int, b: int):
"""
Calculate: Greatest Common Divisor
Arguments:
a {int} -- _description_
b {int} -- _description_
Returns:
_type_ -- _description_
"""
return math.gcd(a, b)
def lcd(a: int, b: int):
"""
Calculate: Least Common Denominator
Arguments:
a {int} -- _description_
b {int} -- _description_
Returns:
_type_ -- _description_
"""
return math.lcm(a, b)
# __END__

View File

@@ -3,32 +3,61 @@ requests lib interface
V2 call type
"""
from typing import Any
import warnings
from typing import Any, TypedDict, cast
import requests
# to hide the verfiy warnings because of the bad SSL settings from Netskope, Akamai, etc
warnings.filterwarnings('ignore', message='Unverified HTTPS request')
from requests import exceptions
class ErrorResponse:
"""
Error response structure. This is returned if a request could not be completed
"""
def __init__(
self,
code: int,
message: str,
action: str,
url: str,
exception: exceptions.InvalidSchema | exceptions.ReadTimeout | exceptions.ConnectionError | None = None
) -> None:
self.code = code
self.message = message
self.action = action
self.url = url
self.exception_name = type(exception).__name__ if exception is not None else None
self.exception_trace = exception if exception is not None else None
class ProxyConfig(TypedDict):
"""
Socks proxy settings
"""
type: str
host: str
port: str
class Caller:
"""_summary_"""
"""
requests lib interface
"""
def __init__(
self,
header: dict[str, str],
verify: bool = True,
timeout: int = 20,
proxy: dict[str, str] | None = None,
proxy: ProxyConfig | None = None,
verify: bool = True,
ca_file: str | None = None
):
self.headers = header
self.timeout: int = timeout
self.cafile = ca_file
self.ca_file = ca_file
self.verify = verify
self.proxy = proxy
self.proxy = cast(dict[str, str], proxy) if proxy is not None else None
def __timeout(self, timeout: int | None) -> int:
if timeout is not None:
if timeout is not None and timeout >= 0:
return timeout
return self.timeout
@@ -39,7 +68,7 @@ class Caller:
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | None:
) -> requests.Response | ErrorResponse:
"""
call wrapper, on error returns None
@@ -56,67 +85,96 @@ class Caller:
if data is None:
data = {}
try:
response = None
if action == "get":
response = requests.get(
return requests.get(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "post":
response = requests.post(
if action == "post":
return requests.post(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "put":
response = requests.put(
if action == "put":
return requests.put(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "patch":
response = requests.patch(
if action == "patch":
return requests.patch(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
elif action == "delete":
response = requests.delete(
if action == "delete":
return requests.delete(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
proxies=self.proxy,
cert=self.ca_file
)
return response
except requests.exceptions.InvalidSchema as e:
print(f"Invalid URL during '{action}' for {url}:\n\t{e}")
return None
except requests.exceptions.ReadTimeout as e:
print(f"Timeout ({self.timeout}s) during '{action}' for {url}:\n\t{e}")
return None
except requests.exceptions.ConnectionError as e:
print(f"Connection error during '{action}' for {url}:\n\t{e}")
return None
return ErrorResponse(
100,
f"Unsupported action '{action}'",
action,
url
)
except exceptions.InvalidSchema as e:
return ErrorResponse(
200,
f"Invalid URL during '{action}' for {url}",
action,
url,
e
)
except exceptions.ReadTimeout as e:
return ErrorResponse(
300,
f"Timeout ({self.timeout}s) during '{action}' for {url}",
action,
url,
e
)
except exceptions.ConnectionError as e:
return ErrorResponse(
400,
f"Connection error during '{action}' for {url}",
action,
url,
e
)
def get(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
def get(
self,
url: str,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
get data
@@ -127,11 +185,15 @@ class Caller:
Returns:
requests.Response: _description_
"""
return self.__call('get', url, params=params)
return self.__call('get', url, params=params, timeout=timeout)
def post(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
post data
@@ -143,11 +205,15 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('post', url, data, params)
return self.__call('post', url, data, params, timeout=timeout)
def put(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""_summary_
Args:
@@ -158,11 +224,15 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('put', url, data, params)
return self.__call('put', url, data, params, timeout=timeout)
def patch(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
self,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""_summary_
Args:
@@ -173,9 +243,14 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('patch', url, data, params)
return self.__call('patch', url, data, params, timeout=timeout)
def delete(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
def delete(
self,
url: str,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | ErrorResponse:
"""
delete
@@ -186,6 +261,6 @@ class Caller:
Returns:
requests.Response | None: _description_
"""
return self.__call('delete', url, params=params)
return self.__call('delete', url, params=params, timeout=timeout)
# __END__

View File

@@ -32,7 +32,7 @@ show_position(file pos optional)
import time
from typing import Literal
from math import floor
from corelibs.datetime_handling.timestamp_convert import convert_timestamp
from corelibs_datetime.timestamp_convert import convert_timestamp
from corelibs.string_handling.byte_helpers import format_bytes

View File

@@ -5,152 +5,14 @@ Set colors with print(f"something {Colors.yellow}colorful{Colors.end})
bold + underline + color combinations are possible.
"""
from warnings import deprecated
from corelibs_text_colors.text_colors import Colors as ColorsNew
class Colors:
@deprecated("Use src.corelibs_text_colors.text_colors instead")
class Colors(ColorsNew):
"""
ANSI colors defined
"""
# General sets, these should not be accessd
__BOLD = '\033[1m'
__UNDERLINE = '\033[4m'
__END = '\033[0m'
__RESET = '\033[0m'
# Define ANSI color codes as class attributes
__BLACK = "\033[30m"
__RED = "\033[31m"
__GREEN = "\033[32m"
__YELLOW = "\033[33m"
__BLUE = "\033[34m"
__MAGENTA = "\033[35m"
__CYAN = "\033[36m"
__WHITE = "\033[37m"
# Define bold/bright versions of the colors
__BLACK_BOLD = "\033[1;30m"
__RED_BOLD = "\033[1;31m"
__GREEN_BOLD = "\033[1;32m"
__YELLOW_BOLD = "\033[1;33m"
__BLUE_BOLD = "\033[1;34m"
__MAGENTA_BOLD = "\033[1;35m"
__CYAN_BOLD = "\033[1;36m"
__WHITE_BOLD = "\033[1;37m"
# BRIGHT, alternative
__BLACK_BRIGHT = '\033[90m'
__RED_BRIGHT = '\033[91m'
__GREEN_BRIGHT = '\033[92m'
__YELLOW_BRIGHT = '\033[93m'
__BLUE_BRIGHT = '\033[94m'
__MAGENTA_BRIGHT = '\033[95m'
__CYAN_BRIGHT = '\033[96m'
__WHITE_BRIGHT = '\033[97m'
# set access vars
bold = __BOLD
underline = __UNDERLINE
end = __END
reset = __RESET
# normal
black = __BLACK
red = __RED
green = __GREEN
yellow = __YELLOW
blue = __BLUE
magenta = __MAGENTA
cyan = __CYAN
white = __WHITE
# bold
black_bold = __BLACK_BOLD
red_bold = __RED_BOLD
green_bold = __GREEN_BOLD
yellow_bold = __YELLOW_BOLD
blue_bold = __BLUE_BOLD
magenta_bold = __MAGENTA_BOLD
cyan_bold = __CYAN_BOLD
white_bold = __WHITE_BOLD
# bright
black_bright = __BLACK_BRIGHT
red_bright = __RED_BRIGHT
green_bright = __GREEN_BRIGHT
yellow_bright = __YELLOW_BRIGHT
blue_bright = __BLUE_BRIGHT
magenta_bright = __MAGENTA_BRIGHT
cyan_bright = __CYAN_BRIGHT
white_bright = __WHITE_BRIGHT
@staticmethod
def disable():
"""
No colors
"""
Colors.bold = ''
Colors.underline = ''
Colors.end = ''
Colors.reset = ''
# normal
Colors.black = ''
Colors.red = ''
Colors.green = ''
Colors.yellow = ''
Colors.blue = ''
Colors.magenta = ''
Colors.cyan = ''
Colors.white = ''
# bold/bright
Colors.black_bold = ''
Colors.red_bold = ''
Colors.green_bold = ''
Colors.yellow_bold = ''
Colors.blue_bold = ''
Colors.magenta_bold = ''
Colors.cyan_bold = ''
Colors.white_bold = ''
# bold/bright alt
Colors.black_bright = ''
Colors.red_bright = ''
Colors.green_bright = ''
Colors.yellow_bright = ''
Colors.blue_bright = ''
Colors.magenta_bright = ''
Colors.cyan_bright = ''
Colors.white_bright = ''
@staticmethod
def reset_colors():
"""
reset colors to the original ones
"""
# set access vars
Colors.bold = Colors.__BOLD
Colors.underline = Colors.__UNDERLINE
Colors.end = Colors.__END
Colors.reset = Colors.__RESET
# normal
Colors.black = Colors.__BLACK
Colors.red = Colors.__RED
Colors.green = Colors.__GREEN
Colors.yellow = Colors.__YELLOW
Colors.blue = Colors.__BLUE
Colors.magenta = Colors.__MAGENTA
Colors.cyan = Colors.__CYAN
Colors.white = Colors.__WHITE
# bold
Colors.black_bold = Colors.__BLACK_BOLD
Colors.red_bold = Colors.__RED_BOLD
Colors.green_bold = Colors.__GREEN_BOLD
Colors.yellow_bold = Colors.__YELLOW_BOLD
Colors.blue_bold = Colors.__BLUE_BOLD
Colors.magenta_bold = Colors.__MAGENTA_BOLD
Colors.cyan_bold = Colors.__CYAN_BOLD
Colors.white_bold = Colors.__WHITE_BOLD
# bright
Colors.black_bright = Colors.__BLACK_BRIGHT
Colors.red_bright = Colors.__RED_BRIGHT
Colors.green_bright = Colors.__GREEN_BRIGHT
Colors.yellow_bright = Colors.__YELLOW_BRIGHT
Colors.blue_bright = Colors.__BLUE_BRIGHT
Colors.magenta_bright = Colors.__MAGENTA_BRIGHT
Colors.cyan_bright = Colors.__CYAN_BRIGHT
Colors.white_bright = Colors.__WHITE_BRIGHT
# __END__

View File

@@ -2,74 +2,24 @@
Enum base classes
"""
from enum import Enum
from typing import Any
import warnings
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
class EnumBase(Enum):
class EnumBase(CorelibsEnumBase):
"""
base for enum
.. deprecated::
Use corelibs_enum_base.EnumBase instead
DEPRECATED: Use corelibs_enum_base.enum_base.EnumBase instead
lookup_any and from_any will return "EnumBase" and the sub class name
run the return again to "from_any" to get a clean value, or cast it
"""
@classmethod
def lookup_key(cls, enum_key: str):
"""Lookup from key side (must be string)"""
# if there is a ":", then this is legacy, replace with ___
if ":" in enum_key:
enum_key = enum_key.replace(':', '___')
try:
return cls[enum_key.upper()]
except KeyError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
except AttributeError as e:
raise ValueError(f"Invalid key: {enum_key}") from e
@classmethod
def lookup_value(cls, enum_value: Any):
"""Lookup through value side"""
try:
return cls(enum_value)
except ValueError as e:
raise ValueError(f"Invalid value: {enum_value}") from e
# At the module level, issue a deprecation warning
warnings.warn("Use corelibs_enum_base.enum_base.EnumBase instead", DeprecationWarning, stacklevel=2)
@classmethod
def from_any(cls, enum_any: Any):
"""
This only works in the following order
-> class itself, as is
-> str, assume key lookup
-> if failed try other
Arguments:
enum_any {Any} -- _description_
Returns:
_type_ -- _description_
"""
if isinstance(enum_any, cls):
return enum_any
# try key first if it is string
# if failed try value
if isinstance(enum_any, str):
try:
return cls.lookup_key(enum_any)
except (ValueError, AttributeError):
try:
return cls.lookup_value(enum_any)
except ValueError as e:
raise ValueError(f"Could not find as key or value: {enum_any}") from e
return cls.lookup_value(enum_any)
def to_value(self) -> Any:
"""Convert to value"""
return self.value
def to_lower_case(self) -> str:
"""return lower case"""
return self.name.lower()
def __str__(self) -> str:
"""return [Enum].NAME like it was called with .name"""
return self.name
# __EMD__

View File

@@ -0,0 +1,15 @@
"""
Enum base classes [STPUB]
"""
from typing_extensions import deprecated
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
@deprecated("Use corelibs_enum_base.enum_base.EnumBase instead")
class EnumBase(CorelibsEnumBase):
"""
base for enum
lookup_any and from_any will return "EnumBase" and the sub class name
run the return again to "from_any" to get a clean value, or cast it
"""

View File

@@ -3,8 +3,11 @@ variable convert, check, etc helepr
"""
from typing import Any
from warnings import deprecated
import corelibs_var.var_helpers
@deprecated("Use corelibs_var.var_helpers.is_int instead")
def is_int(string: Any) -> bool:
"""
check if a value is int
@@ -15,15 +18,10 @@ def is_int(string: Any) -> bool:
Returns:
bool -- _description_
"""
try:
int(string)
return True
except TypeError:
return False
except ValueError:
return False
return corelibs_var.var_helpers.is_int(string)
@deprecated("Use corelibs_var.var_helpers.is_float instead")
def is_float(string: Any) -> bool:
"""
check if a value is float
@@ -34,15 +32,10 @@ def is_float(string: Any) -> bool:
Returns:
bool -- _description_
"""
try:
float(string)
return True
except TypeError:
return False
except ValueError:
return False
return corelibs_var.var_helpers.is_float(string)
@deprecated("Use corelibs_var.var_helpers.str_to_bool instead")
def str_to_bool(string: str):
"""
convert string to bool
@@ -56,10 +49,6 @@ def str_to_bool(string: str):
Returns:
_type_ -- _description_
"""
if string == "True" or string == "true":
return True
if string == "False" or string == "false":
return False
raise ValueError(f"Invalid boolean string: {string}")
return corelibs_var.var_helpers.str_to_bool(string)
# __END__

View File

@@ -0,0 +1,109 @@
"""
Test check andling for regex checks
"""
from corelibs_text_colors.text_colors import Colors
from corelibs.check_handling.regex_constants import (
compile_re, DOMAIN_WITH_LOCALHOST_REGEX, EMAIL_BASIC_REGEX, NAME_EMAIL_BASIC_REGEX, SUB_EMAIL_BASIC_REGEX
)
from corelibs.check_handling.regex_constants_compiled import (
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX, COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX, COMPILED_NAME_EMAIL_BASIC_REGEX
)
NAME_EMAIL_SIMPLE_REGEX = r"""
^\s*(?:"(?P<name1>[^"]+)"\s*<(?P<email1>[^>]+)>|
(?P<name2>.+?)\s*<(?P<email2>[^>]+)>|
<(?P<email3>[^>]+)>|
(?P<email4>[^\s<>]+))\s*$
"""
def domain_test():
"""
domain regex test
"""
print("=" * 30)
test_domains = [
"example.com",
"localhost",
"subdomain.localhost",
"test.localhost.com",
"some-domain.org"
]
regex_domain_check = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
print(f"REGEX: {DOMAIN_WITH_LOCALHOST_REGEX}")
print(f"Check regex: {regex_domain_check.search('localhost')}")
for domain in test_domains:
if regex_domain_check.search(domain):
print(f"Matched: {domain}")
else:
print(f"Did not match: {domain}")
def email_test():
"""
email regex test
"""
print("=" * 30)
email_list = """
e@bar.com
<f@foobar.com>
"Master" <foobar@bar.com>
"not valid" not@valid.com
also not valid not@valid.com
some header <something@bar.com>
test master <master@master.com>
日本語 <japan@jp.net>
"ひほん カケ苦" <foo@bar.com>
single@entry.com
arsch@popsch.com
test open <open@open.com>
"""
print(f"REGEX: SUB_EMAIL_BASIC_REGEX: {SUB_EMAIL_BASIC_REGEX}")
print(f"REGEX: EMAIL_BASIC_REGEX: {EMAIL_BASIC_REGEX}")
print(f"REGEX: COMPILED_NAME_EMAIL_SIMPLE_REGEX: {COMPILED_NAME_EMAIL_SIMPLE_REGEX}")
print(f"REGEX: NAME_EMAIL_BASIC_REGEX: {NAME_EMAIL_BASIC_REGEX}")
basic_email = COMPILED_EMAIL_BASIC_REGEX
sub_basic_email = compile_re(SUB_EMAIL_BASIC_REGEX)
simple_name_email_regex = COMPILED_NAME_EMAIL_SIMPLE_REGEX
full_name_email_regex = COMPILED_NAME_EMAIL_BASIC_REGEX
for email in email_list.splitlines():
email = email.strip()
if not email:
continue
print(f">>> Testing: {email}")
if not basic_email.match(email):
print(f"{Colors.red}[EMAIL ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[EMAIL ] Matched : {email}{Colors.reset}")
if not sub_basic_email.match(email):
print(f"{Colors.red}[SUB ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[SUB ] Matched : {email}{Colors.reset}")
if not simple_name_email_regex.match(email):
print(f"{Colors.red}[SIMPLE] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[SIMPLE] Matched : {email}{Colors.reset}")
if not full_name_email_regex.match(email):
print(f"{Colors.red}[FULL ] No match: {email}{Colors.reset}")
else:
print(f"{Colors.green}[FULL ] Matched : {email}{Colors.reset}")
def main():
"""
Test regex checks
"""
domain_test()
email_test()
if __name__ == "__main__":
main()
# __END__

View File

@@ -1,16 +1,23 @@
[TestA]
foo=bar
overload_from_args=bar
foobar=1
bar=st
arg_overload=should_not_be_set_because_of_command_line_is_list
arg_overload_list=too,be,long
arg_overload_not_set=this should not be set because of override flag
just_values=too,be,long
some_match=foo
some_match_list=foo,bar
test_list=a,b,c,d f, g h
other_list=a|b|c|d|
third_list=xy|ab|df|fg
empty_list=
str_length=foobar
int_range=20
int_range_not_set=
int_range_not_set_empty_set=5
bool_var=True
#
match_target=foo
match_target_list=foo,bar,baz
@@ -32,3 +39,6 @@ email_bad=gii@bar.com
[LoadTest]
a.b.c=foo
d:e:f=bar
[ErrorTest]
some_value=42

View File

@@ -21,11 +21,6 @@ def main():
Main run
"""
value = "2025/1/1"
regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE)
result = regex_c.search(value)
print(f"regex {regex_c} check against {value} -> {result}")
# for log testing
log = Log(
log_path=ROOT_PATH.joinpath(LOG_DIR, 'settings_loader.log'),
@@ -37,9 +32,17 @@ def main():
)
log.logger.info('Settings loader')
value = "2025/1/1"
regex_c = re.compile(SettingsLoaderCheck.CHECK_SETTINGS['string.date']['regex'], re.VERBOSE)
result = regex_c.search(value)
log.info(f"regex {regex_c} check against {value} -> {result}")
sl = SettingsLoader(
{
'foo': 'OVERLOAD'
'overload_from_args': 'OVERLOAD from ARGS',
'arg_overload': ['should', 'not', 'be', 'set'],
'arg_overload_list': ['overload', 'this', 'list'],
'arg_overload_not_set': "DO_NOT_SET",
},
ROOT_PATH.joinpath(CONFIG_DIR, CONFIG_FILE),
log=log
@@ -50,9 +53,11 @@ def main():
config_load,
{
# "doesnt": ["split:,"],
"foo": ["mandatory:yes"],
"overload_from_args": ["args_override:yes", "mandatory:yes"],
"foobar": ["check:int"],
"bar": ["mandatory:yes"],
"arg_overload_list": ["args_override:yes", "split:,",],
"arg_overload_not_set": [],
"some_match": ["matching:foo|bar"],
"some_match_list": ["split:,", "matching:foo|bar"],
"test_list": [
@@ -64,6 +69,9 @@ def main():
"split:|",
"check:string.alphanumeric"
],
"empty_list": [
"split:,",
],
"str_length": [
"length:2-10"
],
@@ -76,6 +84,7 @@ def main():
"int_range_not_set_empty_set": [
"empty:"
],
"bool_var": ["convert:bool"],
"match_target": ["matching:foo"],
"match_target_list": ["split:,", "matching:foo|bar|baz",],
"match_source_a": ["in:match_target"],
@@ -120,6 +129,20 @@ def main():
except ValueError as e:
print(f"Could not load settings: {e}")
try:
config_load = 'ErrorTest'
config_data = sl.load_settings(
config_load,
{
"some_value": [
"check:string.email.basic",
],
}
)
print(f"[{config_load}] Load: {config_load} -> {dump_data(config_data)}")
except ValueError as e:
print(f"Could not load settings: {e}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,139 @@
"""
SQL Main wrapper test
"""
from pathlib import Path
from uuid import uuid4
import json
from corelibs.debug_handling.dump_data import dump_data
from corelibs.logging_handling.log import Log, Logger
from corelibs.db_handling.sql_main import SQLMain
SCRIPT_PATH: Path = Path(__file__).resolve().parent
ROOT_PATH: Path = SCRIPT_PATH
DATABASE_DIR: Path = Path("database")
LOG_DIR: Path = Path("log")
def main() -> None:
"""
Comment
"""
log = Log(
log_path=ROOT_PATH.joinpath(LOG_DIR, 'sqlite_main.log'),
log_name="SQLite Main",
log_settings={
"log_level_console": 'DEBUG',
"log_level_file": 'DEBUG',
}
)
sql_main = SQLMain(
log=Logger(log.get_logger_settings()),
db_ident=f"sqlite:{ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_main.db')}"
)
if sql_main.connected():
log.info("SQL Main connected successfully")
else:
log.error('SQL Main connection failed')
if sql_main.dbh is None:
log.error('SQL Main DBH instance is None')
return
if sql_main.dbh.trigger_exists('trg_test_a_set_date_updated_on_update'):
log.info("Trigger trg_test_a_set_date_updated_on_update exists")
if sql_main.dbh.table_exists('test_a'):
log.info("Table test_a exists, dropping for clean test")
sql_main.dbh.execute_query("DROP TABLE test_a;")
# create a dummy table
table_sql = """
CREATE TABLE IF NOT EXISTS test_a (
test_a_id INTEGER PRIMARY KEY,
date_created TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%f', 'now')),
date_updated TEXT,
uid TEXT NOT NULL UNIQUE,
set_current_timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
text_a TEXT,
content,
int_a INTEGER,
float_a REAL
);
"""
result = sql_main.dbh.execute_query(table_sql)
log.debug(f"Create table result: {result}")
trigger_sql = """
CREATE TRIGGER trg_test_a_set_date_updated_on_update
AFTER UPDATE ON test_a
FOR EACH ROW
WHEN OLD.date_updated IS NULL OR NEW.date_updated = OLD.date_updated
BEGIN
UPDATE test_a
SET date_updated = (strftime('%Y-%m-%d %H:%M:%f', 'now'))
WHERE test_a_id = NEW.test_a_id;
END;
"""
result = sql_main.dbh.execute_query(trigger_sql)
log.debug(f"Create trigger result: {result}")
result = sql_main.dbh.meta_data_detail('test_a')
log.debug(f"Table meta data detail: {dump_data(result)}")
# INSERT DATA
sql = """
INSERT INTO test_a (uid, text_a, content, int_a, float_a)
VALUES (?, ?, ?, ?, ?)
RETURNING test_a_id, uid;
"""
result = sql_main.dbh.execute_query(
sql,
(
str(uuid4()),
'Some text A',
json.dumps({'foo': 'bar', 'number': 42}),
123,
123.456,
)
)
log.debug(f"[1] Insert data result: {dump_data(result)}")
__uid: str = ''
if result is not False:
# first one only of interest
result = dict(result[0])
__uid = str(result.get('uid', ''))
# second insert
result = sql_main.dbh.execute_query(
sql,
(
str(uuid4()),
'Some text A',
json.dumps({'foo': 'bar', 'number': 42}),
123,
123.456,
)
)
log.debug(f"[2] Insert data result: {dump_data(result)}")
result = sql_main.dbh.execute_query("SELECT * FROM test_a;")
log.debug(f"Select data result: {dump_data(result)}")
result = sql_main.dbh.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
log.debug(f"Fetch row result: {dump_data(result)}")
sql = """
UPDATE test_a
SET text_a = ?
WHERE uid = ?;
"""
result = sql_main.dbh.execute_query(
sql,
(
'Some updated text A',
__uid,
)
)
log.debug(f"Update data result: {dump_data(result)}")
result = sql_main.dbh.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
log.debug(f"Fetch row after update result: {dump_data(result)}")
sql_main.close()
if __name__ == "__main__":
main()
# __END__

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
"""
Main comment
SQLite IO test
"""
from pathlib import Path

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""
BOM check for files
"""
from pathlib import Path
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
from corelibs.debug_handling.dump_data import dump_data
def main() -> None:
"""
Check files for BOM encoding
"""
base_path = Path(__file__).resolve().parent
for file_path in [
'test-data/sample_with_bom.csv',
'test-data/sample_without_bom.csv',
]:
has_bom = is_bom_encoded(base_path.joinpath(file_path))
bom_info = is_bom_encoded_info(base_path.joinpath(file_path))
print(f'File: {file_path}')
print(f' Has BOM: {has_bom}')
print(f' BOM Info: {dump_data(bom_info)}')
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,6 @@
Name,Age,City,Country
John Doe,25,New York,USA
Jane Smith,30,London,UK
山田太郎,28,東京,Japan
María García,35,Madrid,Spain
François Dupont,42,Paris,France
1 Name Age City Country
2 John Doe 25 New York USA
3 Jane Smith 30 London UK
4 山田太郎 28 東京 Japan
5 María García 35 Madrid Spain
6 François Dupont 42 Paris France

View File

@@ -0,0 +1,6 @@
Name,Age,City,Country
John Doe,25,New York,USA
Jane Smith,30,London,UK
山田太郎,28,東京,Japan
María García,35,Madrid,Spain
François Dupont,42,Paris,France
1 Name Age City Country
2 John Doe 25 New York USA
3 Jane Smith 30 London UK
4 山田太郎 28 東京 Japan
5 María García 35 Madrid Spain
6 François Dupont 42 Paris France

View File

@@ -24,12 +24,19 @@ def main() -> None:
"lookup_value_c": "B02",
"replace_value": "R02",
},
{
"lookup_value_p": "A03",
"lookup_value_c": "B03",
"replace_value": "R03",
},
]
test_foo = ArraySearchList(
key = "lookup_value_p",
value = "A01"
key="lookup_value_p",
value="A01"
)
print(test_foo)
result = find_in_array_from_list(data, [test_foo])
print(f"Search A: {dump_data(test_foo)} -> {dump_data(result)}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
@@ -38,12 +45,122 @@ def main() -> None:
{
"key": "lookup_value_c",
"value": "B01"
},
]
result = find_in_array_from_list(data, search)
print(f"Search B: {dump_data(search)} -> {dump_data(result)}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "A01"
},
{
"key": "lookup_value_c",
"value": "B01"
},
{
"key": "lookup_value_c",
"value": "B02"
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search C: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search C raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "A01"
},
{
"key": "lookup_value_c",
"value": ["B01", "B02"]
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search D: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search D raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": ["A01", "A03"]
},
{
"key": "lookup_value_c",
"value": ["B01", "B02"]
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search E: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search E raised KeyError: {e}")
search: list[ArraySearchList] = [
{
"key": "lookup_value_p",
"value": "NOT FOUND"
},
]
try:
result = find_in_array_from_list(data, search)
print(f"Search F: {dump_data(search)} -> {dump_data(result)}")
except KeyError as e:
print(f"Search F raised KeyError: {e}")
data = [
{
"sd_user_id": "1593",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1592",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1596",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1594",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1595",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1861",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1862",
"email": "",
"employee_id": ""
},
{
"sd_user_id": "1860",
"email": "",
"employee_id": ""
}
]
result = find_in_array_from_list(data, search)
print(f"Search {dump_data(search)} -> {dump_data(result)}")
result = find_in_array_from_list(data, [ArraySearchList(
key="sd_user_id",
value="1593"
)])
print(f"Search F: -> {dump_data(result)}")
if __name__ == "__main__":

View File

@@ -2,7 +2,10 @@
test list helpers
"""
from corelibs.iterator_handling.list_helpers import is_list_in_list, convert_to_list
from typing import Any
from corelibs.debug_handling.dump_data import dump_data
from corelibs.iterator_handling.list_helpers import is_list_in_list, convert_to_list, make_unique_list_of_dicts
from corelibs.iterator_handling.fingerprint import dict_hash_crc
def __test_is_list_in_list_a():
@@ -18,9 +21,66 @@ def __convert_list():
print(f"IN: {source} -> {result}")
def __make_unique_list_of_dicts():
dict_list = [
{"a": 1, "b": 2, "nested": {"x": 10, "y": 20}},
{"a": 1, "b": 2, "nested": {"x": 10, "y": 20}},
{"b": 2, "a": 1, "nested": {"y": 20, "x": 10}},
{"b": 2, "a": 1, "nested": {"y": 20, "x": 30}},
{"a": 3, "b": 4, "nested": {"x": 30, "y": 40}}
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list = [
{"a": 1, 1: "one"},
{1: "one", "a": 1},
{"a": 2, 1: "one"}
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list = [
{"a": 1, "b": [1, 2, 3]},
{"b": [1, 2, 3], "a": 1},
{"a": 1, "b": [1, 2, 4]},
1, 2, "String", 1, "Foobar"
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list: list[Any] = [
[],
{},
[],
{},
{"a": []},
{"a": []},
{"a": {}},
{"a": {}},
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
dict_list: list[Any] = [
(1, 2),
(1, 2),
(2, 3),
]
unique_dicts = make_unique_list_of_dicts(dict_list)
dhf = dict_hash_crc(unique_dicts)
print(f"Unique dicts: {dump_data(unique_dicts)} [{dhf}]")
def main():
"""List helpers test runner"""
__test_is_list_in_list_a()
__convert_list()
__make_unique_list_of_dicts()
if __name__ == "__main__":

View File

@@ -6,7 +6,7 @@ Log logging_handling.log testing
import sys
from pathlib import Path
# this is for testing only
from corelibs.logging_handling.log import Log, Logger
from corelibs.logging_handling.log import Log, Logger, ConsoleFormat, ConsoleFormatSettings
from corelibs.debug_handling.debug_helpers import exception_stack, call_stack
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -24,11 +24,23 @@ def main():
# "log_level_console": None,
"log_level_file": 'DEBUG',
# "console_color_output_enabled": False,
"per_run_log": True
"per_run_log": True,
# "console_format_type": ConsoleFormatSettings.NONE,
# "console_format_type": ConsoleFormatSettings.MINIMAL,
# "console_format_type": ConsoleFormat.TIME_MICROSECONDS | ConsoleFormat.NAME | ConsoleFormat.LEVEL,
"console_format_type": None,
# "console_format_type": ConsoleFormat.NAME,
# "console_format_type": (
# ConsoleFormat.TIME | ConsoleFormat.TIMEZONE | ConsoleFormat.LINENO | ConsoleFormat.LEVEL
# ),
}
)
logn = Logger(log.get_logger_settings())
log.info("ConsoleFormatType FILE is: %s", ConsoleFormat.FILE)
log.info("ConsoleFormatSettings ALL is: %s", ConsoleFormatSettings.ALL)
log.info("ConsoleFormatSettings lookup is: %s", ConsoleFormatSettings.from_string('ALL'))
log.logger.debug('[NORMAL] Debug test: %s', log.logger.name)
log.lg.debug('[NORMAL] Debug test: %s', log.logger.name)
log.debug('[NORMAL-] Debug test: %s', log.logger.name)
@@ -94,10 +106,34 @@ def main():
for key, handler in log.handlers.items():
print(f"Handler (handlers) [{key}] {handler} -> {handler.level} -> {LoggingLevel.from_any(handler.level)}")
log.set_log_level('stream_handler', LoggingLevel.ERROR)
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.ERROR)
log.logger.warning('[NORMAL] Invisible Warning test: %s', log.logger.name)
log.logger.error('[NORMAL] Visible Error test: %s', log.logger.name)
# log.handlers['stream_handler'].se
log.logger.debug('[NORMAL] Visible Debug test: %s', log.logger.name)
print(f"*** Any handler is minimum level ERROR: {log.any_handler_is_minimum_level(LoggingLevel.ERROR)}")
print(f"*** Any handler is minimum level DEBUG: {log.any_handler_is_minimum_level(LoggingLevel.DEBUG)}")
for handler in log.handlers.values():
print(
f"*** Setting handler {handler} is level {LoggingLevel.from_any(handler.level).name} -> "
f"*** INC {LoggingLevel.from_any(handler.level).includes(LoggingLevel.DEBUG)}")
print(f"*** WARNING includes ERROR: {LoggingLevel.WARNING.includes(LoggingLevel.ERROR)}")
print(f"*** ERROR includes WARNING: {LoggingLevel.ERROR.includes(LoggingLevel.WARNING)}")
log.set_log_level(Log.CONSOLE_HANDLER, LoggingLevel.DEBUG)
log.debug('Current logging format: %s', log.log_settings['console_format_type'])
log.debug('Current console formatter: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less A')
log.debug('Current console formatter after A: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormat.TIME | ConsoleFormat.LINENO)
log.info('Does hit show less B')
log.debug('Current console formatter after B: %s', log.get_console_formatter())
log.update_console_formatter(ConsoleFormatSettings.ALL)
log.info('Does hit show less C')
log.debug('Current console formatter after C: %s', log.get_console_formatter())
print(f"*** Any handler is minimum level ERROR: {log.any_handler_is_minimum_level(LoggingLevel.ERROR)}")
print(f"*** Any handler is minimum level DEBUG: {log.any_handler_is_minimum_level(LoggingLevel.DEBUG)}")
if __name__ == "__main__":

View File

View File

@@ -8,10 +8,21 @@ import re
import pytest
from corelibs.check_handling.regex_constants import (
compile_re,
SUB_EMAIL_BASIC_REGEX,
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX,
DOMAIN_REGEX
)
from corelibs.check_handling.regex_constants_compiled import (
COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX,
COMPILED_NAME_EMAIL_BASIC_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX,
COMPILED_DOMAIN_REGEX,
)
@@ -48,7 +59,7 @@ class TestEmailBasicRegex:
@pytest.fixture
def email_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled email regex pattern."""
return compile_re(EMAIL_BASIC_REGEX)
return COMPILED_EMAIL_BASIC_REGEX
@pytest.mark.parametrize("valid_email", [
"user@example.com",
@@ -123,13 +134,272 @@ class TestEmailBasicRegex:
assert not email_pattern.match(email)
class TestSubEmailBasicRegex:
"""Test cases for SUB_EMAIL_BASIC_REGEX pattern (without anchors)."""
@pytest.fixture
def sub_email_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled sub email regex pattern."""
return compile_re(rf"^{SUB_EMAIL_BASIC_REGEX}$")
@pytest.mark.parametrize("valid_email", [
"user@example.com",
"test.user@example.com",
"user+tag@example.co.uk",
"first.last@subdomain.example.com",
"user123@test-domain.com",
"a@example.com",
"user_name@example.com",
"user-name@example.com",
"user@sub.domain.example.com",
"test!#$%&'*+-/=?^_`{|}~@example.com",
"1234567890@example.com",
])
def test_valid_emails_match(self, sub_email_pattern: re.Pattern[str], valid_email: str) -> None:
"""Test that valid email addresses match SUB_EMAIL_BASIC_REGEX."""
assert sub_email_pattern.match(valid_email), (
f"Failed to match valid email: {valid_email}"
)
@pytest.mark.parametrize("invalid_email", [
"",
"@example.com",
"user@",
"user",
"user@.com",
"user@domain",
"user @example.com",
".user@example.com",
"user@-example.com",
"user@example-.com",
"user@example.c",
"user@example.toolong",
])
def test_invalid_emails_no_match(self, sub_email_pattern: re.Pattern[str], invalid_email: str) -> None:
"""Test that invalid emails don't match SUB_EMAIL_BASIC_REGEX."""
assert not sub_email_pattern.match(invalid_email), (
f"Incorrectly matched invalid email: {invalid_email}"
)
def test_sub_email_max_local_part_length(self, sub_email_pattern: re.Pattern[str]) -> None:
"""Test email with maximum local part length (64 characters)."""
local_part = "a" * 64
email = f"{local_part}@example.com"
assert sub_email_pattern.match(email)
def test_sub_email_exceeds_local_part_length(self, sub_email_pattern: re.Pattern[str]) -> None:
"""Test email exceeding maximum local part length."""
local_part = "a" * 65
email = f"{local_part}@example.com"
assert not sub_email_pattern.match(email)
class TestNameEmailSimpleRegex:
"""Test cases for NAME_EMAIL_SIMPLE_REGEX pattern."""
@pytest.fixture
def name_email_simple_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled name+email simple regex pattern."""
return COMPILED_NAME_EMAIL_SIMPLE_REGEX
@pytest.mark.parametrize("test_input,expected_groups", [
('"John Doe" <john@example.com>', {'name1': 'John Doe', 'email1': 'john@example.com'}),
('John Doe <john@example.com>', {'name2': 'John Doe', 'email2': 'john@example.com'}),
('<john@example.com>', {'email3': 'john@example.com'}),
('john@example.com', {'email4': 'john@example.com'}),
(' "Jane Smith" <jane@test.com> ', {'name1': 'Jane Smith', 'email1': 'jane@test.com'}),
('Bob <bob@test.org>', {'name2': 'Bob', 'email2': 'bob@test.org'}),
])
def test_valid_name_email_combinations(
self, name_email_simple_pattern: re.Pattern[str], test_input: str, expected_groups: dict[str, str]
) -> None:
"""Test that valid name+email combinations match and extract correct groups."""
match = name_email_simple_pattern.match(test_input)
assert match is not None, f"Failed to match: {test_input}"
# Check that expected groups are present and match
for group_name, expected_value in expected_groups.items():
assert match.group(group_name) == expected_value, (
f"Group {group_name} expected '{expected_value}', got '{match.group(group_name)}'"
)
@pytest.mark.parametrize("invalid_input", [
"",
"not an email",
"<>",
'"Name Only"',
'Name <',
'<email',
'Name <<email@test.com>>',
'Name <email@test.com',
'Name email@test.com>',
])
def test_invalid_name_email_combinations(
self, name_email_simple_pattern: re.Pattern[str], invalid_input: str
) -> None:
"""Test that invalid inputs don't match NAME_EMAIL_SIMPLE_REGEX."""
assert not name_email_simple_pattern.match(invalid_input), (
f"Incorrectly matched invalid input: {invalid_input}"
)
def test_extract_name_from_quoted(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test extracting name from quoted format."""
match = name_email_simple_pattern.match('"Alice Wonder" <alice@example.com>')
assert match is not None
assert match.group('name1') == 'Alice Wonder'
assert match.group('email1') == 'alice@example.com'
def test_extract_name_from_unquoted(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test extracting name from unquoted format."""
match = name_email_simple_pattern.match('Bob Builder <bob@example.com>')
assert match is not None
assert match.group('name2') == 'Bob Builder'
assert match.group('email2') == 'bob@example.com'
def test_email_only_in_brackets(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test email-only format in angle brackets."""
match = name_email_simple_pattern.match('<charlie@example.com>')
assert match is not None
assert match.group('email3') == 'charlie@example.com'
def test_email_only_plain(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test plain email format without brackets."""
match = name_email_simple_pattern.match('dave@example.com')
assert match is not None
assert match.group('email4') == 'dave@example.com'
def test_whitespace_handling(
self, name_email_simple_pattern: re.Pattern[str]
) -> None:
"""Test that leading/trailing whitespace is handled correctly."""
match = name_email_simple_pattern.match(' "User Name" <user@example.com> ')
assert match is not None
assert match.group('name1') == 'User Name'
assert match.group('email1') == 'user@example.com'
class TestNameEmailBasicRegex:
"""Test cases for NAME_EMAIL_BASIC_REGEX pattern with strict email validation."""
@pytest.fixture
def name_email_basic_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled name+email basic regex pattern."""
return COMPILED_NAME_EMAIL_BASIC_REGEX
@pytest.mark.parametrize("test_input,expected_name,expected_email", [
('"John Doe" <john@example.com>', 'John Doe', 'john@example.com'),
('John Doe <john@example.com>', 'John Doe', 'john@example.com'),
('<john@example.com>', None, 'john@example.com'),
('john@example.com', None, 'john@example.com'),
(' "Jane Smith" <jane.smith@test.co.uk> ', 'Jane Smith', 'jane.smith@test.co.uk'),
('Alice Wonder <alice+tag@example.com>', 'Alice Wonder', 'alice+tag@example.com'),
])
def test_valid_name_email_with_validation(
self,
name_email_basic_pattern: re.Pattern[str],
test_input: str,
expected_name: str | None,
expected_email: str,
) -> None:
"""Test valid name+email with strict email validation."""
match = name_email_basic_pattern.match(test_input)
assert match is not None, f"Failed to match: {test_input}"
# Extract name and email from whichever group matched
name = match.group('name1') or match.group('name2')
email = (
match.group('email1') or match.group('email2') or
match.group('email3') or match.group('email4')
)
assert name == expected_name, f"Expected name '{expected_name}', got '{name}'"
assert email == expected_email, f"Expected email '{expected_email}', got '{email}'"
@pytest.mark.parametrize("invalid_input", [
'"John Doe" <invalid.email>', # invalid email format
'John Doe <@example.com>', # missing local part
'<user@>', # missing domain
'user@domain', # no TLD
'"Name" <user @example.com>', # space in email
'<.user@example.com>', # starts with dot
'user@-example.com', # domain starts with hyphen
'Name <user@example.c>', # TLD too short
'Name <user@example.toolongdomain>', # TLD too long
])
def test_invalid_email_format_rejected(
self, name_email_basic_pattern: re.Pattern[str], invalid_input: str
) -> None:
"""Test that inputs with invalid email formats are rejected."""
assert not name_email_basic_pattern.match(invalid_input), (
f"Incorrectly matched invalid input: {invalid_input}"
)
def test_quoted_name_with_valid_email(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test quoted name format with valid email."""
match = name_email_basic_pattern.match('"Alice Wonder" <alice@example.com>')
assert match is not None
assert match.group('name1') == 'Alice Wonder'
assert match.group('email1') == 'alice@example.com'
def test_unquoted_name_with_valid_email(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test unquoted name format with valid email."""
match = name_email_basic_pattern.match('Bob Builder <bob@example.com>')
assert match is not None
assert match.group('name2') == 'Bob Builder'
assert match.group('email2') == 'bob@example.com'
def test_email_only_formats(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test email-only formats (with and without brackets)."""
# With brackets
match1 = name_email_basic_pattern.match('<charlie@example.com>')
assert match1 is not None
assert match1.group('email3') == 'charlie@example.com'
# Without brackets
match2 = name_email_basic_pattern.match('dave@example.com')
assert match2 is not None
assert match2.group('email4') == 'dave@example.com'
def test_whitespace_handling(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test that leading/trailing whitespace is handled correctly."""
match = name_email_basic_pattern.match(' "User" <user@example.com> ')
assert match is not None
assert match.group('name1') == 'User'
assert match.group('email1') == 'user@example.com'
def test_special_characters_in_local_part(
self, name_email_basic_pattern: re.Pattern[str]
) -> None:
"""Test email with special characters in local part."""
match = name_email_basic_pattern.match('Test User <test!#$%&\'*+-/=?^_`{|}~@example.com>')
assert match is not None
assert match.group('name2') == 'Test User'
assert match.group('email2') == 'test!#$%&\'*+-/=?^_`{|}~@example.com'
class TestDomainWithLocalhostRegex:
"""Test cases for DOMAIN_WITH_LOCALHOST_REGEX pattern."""
@pytest.fixture
def domain_localhost_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain with localhost regex pattern."""
return compile_re(DOMAIN_WITH_LOCALHOST_REGEX)
return COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
@pytest.mark.parametrize("valid_domain", [
"localhost",
@@ -181,7 +451,7 @@ class TestDomainWithLocalhostPortRegex:
@pytest.fixture
def domain_localhost_port_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain and localhost with port pattern."""
return compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX)
return COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX
@pytest.mark.parametrize("valid_domain", [
"localhost",
@@ -247,7 +517,7 @@ class TestDomainRegex:
@pytest.fixture
def domain_pattern(self) -> re.Pattern[str]:
"""Fixture that returns compiled domain regex pattern."""
return compile_re(DOMAIN_REGEX)
return COMPILED_DOMAIN_REGEX
@pytest.mark.parametrize("valid_domain", [
"example.com",
@@ -306,6 +576,8 @@ class TestRegexPatternConsistency:
"""Test that all regex patterns can be compiled without errors."""
patterns = [
EMAIL_BASIC_REGEX,
NAME_EMAIL_SIMPLE_REGEX,
NAME_EMAIL_BASIC_REGEX,
DOMAIN_WITH_LOCALHOST_REGEX,
DOMAIN_WITH_LOCALHOST_PORT_REGEX,
DOMAIN_REGEX,
@@ -314,9 +586,24 @@ class TestRegexPatternConsistency:
compiled = compile_re(pattern)
assert isinstance(compiled, re.Pattern)
def test_compiled_patterns_are_patterns(self) -> None:
"""Test that all COMPILED_ constants are Pattern objects."""
compiled_patterns = [
COMPILED_EMAIL_BASIC_REGEX,
COMPILED_NAME_EMAIL_SIMPLE_REGEX,
COMPILED_NAME_EMAIL_BASIC_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX,
COMPILED_DOMAIN_REGEX,
]
for pattern in compiled_patterns:
assert isinstance(pattern, re.Pattern)
def test_domain_patterns_are_strings(self) -> None:
"""Test that all regex constants are strings."""
assert isinstance(EMAIL_BASIC_REGEX, str)
assert isinstance(NAME_EMAIL_SIMPLE_REGEX, str)
assert isinstance(NAME_EMAIL_BASIC_REGEX, str)
assert isinstance(DOMAIN_WITH_LOCALHOST_REGEX, str)
assert isinstance(DOMAIN_WITH_LOCALHOST_PORT_REGEX, str)
assert isinstance(DOMAIN_REGEX, str)
@@ -325,8 +612,8 @@ class TestRegexPatternConsistency:
"""Test that domain patterns follow expected hierarchy."""
# DOMAIN_WITH_LOCALHOST_PORT_REGEX should accept everything
# DOMAIN_WITH_LOCALHOST_REGEX accepts
domain_localhost = compile_re(DOMAIN_WITH_LOCALHOST_REGEX)
domain_localhost_port = compile_re(DOMAIN_WITH_LOCALHOST_PORT_REGEX)
domain_localhost = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX
domain_localhost_port = COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX
test_cases = ["example.com", "subdomain.example.com", "localhost"]
for test_case in test_cases:

View File

@@ -16,7 +16,7 @@ class TestSettingsLoaderInit:
def test_init_with_valid_config_file(self, tmp_path: Path):
"""Test initialization with a valid config file"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
loader = SettingsLoader(
@@ -35,7 +35,7 @@ class TestSettingsLoaderInit:
def test_init_with_missing_config_file(self, tmp_path: Path):
"""Test initialization with missing config file"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(
args={},
@@ -60,7 +60,7 @@ class TestSettingsLoaderInit:
def test_init_with_log(self, tmp_path: Path):
"""Test initialization with Log object"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
mock_log = Mock(spec=Log)
@@ -80,7 +80,7 @@ class TestLoadSettings:
def test_load_settings_basic(self, tmp_path: Path):
"""Test loading basic settings without validation"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nkey1=value1\nkey2=value2\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -90,7 +90,7 @@ class TestLoadSettings:
def test_load_settings_with_missing_section(self, tmp_path: Path):
"""Test loading settings with missing section"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -100,7 +100,7 @@ class TestLoadSettings:
def test_load_settings_allow_not_exist(self, tmp_path: Path):
"""Test loading settings with allow_not_exist flag"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -110,7 +110,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_present(self, tmp_path: Path):
"""Test mandatory field validation when field is present"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -123,7 +123,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_missing(self, tmp_path: Path):
"""Test mandatory field validation when field is missing"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -136,7 +136,7 @@ class TestLoadSettings:
def test_load_settings_mandatory_field_empty(self, tmp_path: Path):
"""Test mandatory field validation when field is empty"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -149,7 +149,7 @@ class TestLoadSettings:
def test_load_settings_with_split(self, tmp_path: Path):
"""Test splitting values into lists"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a,b,c,d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -162,7 +162,7 @@ class TestLoadSettings:
def test_load_settings_with_custom_split_char(self, tmp_path: Path):
"""Test splitting with custom delimiter"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a|b|c|d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -175,7 +175,7 @@ class TestLoadSettings:
def test_load_settings_split_removes_spaces(self, tmp_path: Path):
"""Test that split removes spaces from values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a, b , c , d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -188,7 +188,7 @@ class TestLoadSettings:
def test_load_settings_empty_split_char_fallback(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test fallback to default split char when empty"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a,b,c\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -201,9 +201,22 @@ class TestLoadSettings:
captured = capsys.readouterr()
assert "fallback to:" in captured.out
def test_load_settings_split_empty_value(self, tmp_path: Path):
"""Test that split on empty value results in empty list"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:,"]}
)
assert result["list_field"] == []
def test_load_settings_convert_to_int(self, tmp_path: Path):
"""Test converting values to int"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -217,7 +230,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_float(self, tmp_path: Path):
"""Test converting values to float"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123.45\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -231,7 +244,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_bool_true(self, tmp_path: Path):
"""Test converting values to boolean True"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=true\nflag2=True\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -245,7 +258,7 @@ class TestLoadSettings:
def test_load_settings_convert_to_bool_false(self, tmp_path: Path):
"""Test converting values to boolean False"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=false\nflag2=False\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -259,7 +272,7 @@ class TestLoadSettings:
def test_load_settings_convert_invalid_type(self, tmp_path: Path):
"""Test converting with invalid type raises error"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -272,7 +285,7 @@ class TestLoadSettings:
def test_load_settings_empty_set_to_none(self, tmp_path: Path):
"""Test setting empty values to None"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -285,7 +298,7 @@ class TestLoadSettings:
def test_load_settings_empty_set_to_custom_value(self, tmp_path: Path):
"""Test setting empty values to custom value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -298,7 +311,7 @@ class TestLoadSettings:
def test_load_settings_matching_valid(self, tmp_path: Path):
"""Test matching validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=production\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -311,7 +324,7 @@ class TestLoadSettings:
def test_load_settings_matching_invalid(self, tmp_path: Path):
"""Test matching validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=invalid\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -324,7 +337,7 @@ class TestLoadSettings:
def test_load_settings_in_valid(self, tmp_path: Path):
"""Test 'in' validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=b\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -340,7 +353,7 @@ class TestLoadSettings:
def test_load_settings_in_invalid(self, tmp_path: Path):
"""Test 'in' validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nallowed=a,b,c\nvalue=d\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -356,7 +369,7 @@ class TestLoadSettings:
def test_load_settings_in_missing_target(self, tmp_path: Path):
"""Test 'in' validation with missing target"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -369,7 +382,7 @@ class TestLoadSettings:
def test_load_settings_length_exact(self, tmp_path: Path):
"""Test length validation with exact match"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -382,7 +395,7 @@ class TestLoadSettings:
def test_load_settings_length_exact_invalid(self, tmp_path: Path):
"""Test length validation with exact match failure"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -395,7 +408,7 @@ class TestLoadSettings:
def test_load_settings_length_range(self, tmp_path: Path):
"""Test length validation with range"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -408,7 +421,7 @@ class TestLoadSettings:
def test_load_settings_length_min_only(self, tmp_path: Path):
"""Test length validation with minimum only"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -421,7 +434,7 @@ class TestLoadSettings:
def test_load_settings_length_max_only(self, tmp_path: Path):
"""Test length validation with maximum only"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -434,7 +447,7 @@ class TestLoadSettings:
def test_load_settings_range_valid(self, tmp_path: Path):
"""Test range validation with valid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=25\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -447,7 +460,7 @@ class TestLoadSettings:
def test_load_settings_range_invalid(self, tmp_path: Path):
"""Test range validation with invalid value"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=100\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -460,7 +473,7 @@ class TestLoadSettings:
def test_load_settings_check_int_valid(self, tmp_path: Path):
"""Test check:int with valid integer"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12345\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -473,7 +486,7 @@ class TestLoadSettings:
def test_load_settings_check_int_cleanup(self, tmp_path: Path):
"""Test check:int with cleanup"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12a34b5\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -486,7 +499,7 @@ class TestLoadSettings:
def test_load_settings_check_email_valid(self, tmp_path: Path):
"""Test check:string.email.basic with valid email"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=test@example.com\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -499,7 +512,7 @@ class TestLoadSettings:
def test_load_settings_check_email_invalid(self, tmp_path: Path):
"""Test check:string.email.basic with invalid email"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=not-an-email\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -512,7 +525,7 @@ class TestLoadSettings:
def test_load_settings_args_override(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test command line arguments override config values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
@@ -528,9 +541,126 @@ class TestLoadSettings:
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_no_flag(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with list argument that has split"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
args={"value": ["x", "y", "z"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["split:,"]}
)
# Without args_override:yes flag, should use config value (no override)
assert result["value"] == ["a", "b", "c"]
captured = capsys.readouterr()
# Message is printed but without args_override:yes flag, override doesn't happen
assert "Command line option override" in captured.out
def test_load_settings_args_list_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments without split entry are skipped"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2", "arg3"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": []}
)
# Should keep config value since args is list but no split defined
assert result["value"] == "config_value"
captured = capsys.readouterr()
# Message is printed but list without split prevents the override
assert "Command line option override" in captured.out
def test_load_settings_args_list_with_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that list arguments with split entry and args_override:yes are applied"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a,b,c\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2", "arg3"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["split:,", "args_override:yes"]}
)
# Should use args value because split is defined AND args_override:yes is set
assert result["value"] == ["arg1", "arg2", "arg3"]
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_no_with_mandatory(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior (no args_override:yes) with mandatory field and list args with split"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config1,config2\n")
loader = SettingsLoader(
args={"value": ["arg1", "arg2"]},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["mandatory:yes", "split:,"]}
)
# Should use config value because args_override:yes is not set (default: no override)
assert result["value"] == ["config1", "config2"]
captured = capsys.readouterr()
# Message is printed but without args_override:yes flag, override doesn't happen
assert "Command line option override" in captured.out
def test_load_settings_args_no_with_mandatory_valid(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test default behavior with string args (always overrides due to current logic)"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": "arg_value"},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["mandatory:yes"]}
)
# Current behavior: string args without split always override (regardless of args_override:yes)
assert result["value"] == "arg_value"
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_args_string_no_split(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test that string arguments with args_override:yes work normally"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=config_value\n")
loader = SettingsLoader(
args={"value": "arg_value"},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"value": ["args_override:yes"]}
)
# Should use args value for non-list args with args_override:yes
assert result["value"] == "arg_value"
captured = capsys.readouterr()
assert "Command line option override" in captured.out
def test_load_settings_no_config_file_with_args(self, tmp_path: Path):
"""Test loading settings without config file but with mandatory args"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(
args={"required": "value"},
@@ -545,7 +675,7 @@ class TestLoadSettings:
def test_load_settings_no_config_file_missing_args(self, tmp_path: Path):
"""Test loading settings without config file and missing args"""
config_file = tmp_path / "missing.ini"
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -557,7 +687,7 @@ class TestLoadSettings:
def test_load_settings_check_list_with_split(self, tmp_path: Path):
"""Test check validation with list values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist=abc,def,ghi\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -570,7 +700,7 @@ class TestLoadSettings:
def test_load_settings_check_list_cleanup(self, tmp_path: Path):
"""Test check validation cleans up list values"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist=ab-c,de_f,gh!i\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -583,7 +713,7 @@ class TestLoadSettings:
def test_load_settings_invalid_check_type(self, tmp_path: Path):
"""Test with invalid check type"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
@@ -600,7 +730,7 @@ class TestComplexScenarios:
def test_complex_validation_scenario(self, tmp_path: Path):
"""Test complex scenario with multiple validations"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Production]\n"
"environment=production\n"
@@ -641,7 +771,7 @@ class TestComplexScenarios:
def test_email_list_validation(self, tmp_path: Path):
"""Test email list with validation"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[EmailConfig]\n"
"emails=test@example.com,admin@domain.org,user+tag@site.co.uk\n"
@@ -658,7 +788,7 @@ class TestComplexScenarios:
def test_mixed_args_and_config(self, tmp_path: Path):
"""Test mixing command line args and config file"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Settings]\n"
"value1=config_value1\n"
@@ -679,7 +809,7 @@ class TestComplexScenarios:
def test_multiple_check_types(self, tmp_path: Path):
"""Test multiple different check types"""
config_file = tmp_path / "test.ini"
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Checks]\n"
"numbers=123,456,789\n"
@@ -704,5 +834,48 @@ class TestComplexScenarios:
assert result["emails"] == "test@example.com"
assert result["date"] == "2025-01-15"
def test_args_no_and_list_skip_combination(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test combination of args_override:yes flag and list argument skip behavior"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Settings]\n"
"no_override=a,b,c\n"
"list_no_split=config_list\n"
"list_with_split=x,y,z\n"
"normal=config_normal\n"
)
loader = SettingsLoader(
args={
"no_override": ["arg1", "arg2"],
"list_no_split": ["arg1", "arg2"],
"list_with_split": ["p", "q", "r"],
"normal": "arg_normal"
},
config_file=config_file
)
result = loader.load_settings(
"Settings",
{
"no_override": ["split:,"],
"list_no_split": [],
"list_with_split": ["split:,", "args_override:yes"],
"normal": ["args_override:yes"]
}
)
# Should use config value (no args_override:yes flag for list with split)
assert result["no_override"] == ["a", "b", "c"]
# Should use config value because args is list without split
assert result["list_no_split"] == "config_list"
# Should use args value because split is defined AND args_override:yes is set
assert result["list_with_split"] == ["p", "q", "r"]
# Should use args value (args_override:yes set for string arg)
assert result["normal"] == "arg_normal"
captured = capsys.readouterr()
# Should see override messages (even though list_no_split prints, it doesn't apply)
assert "Command line option override" in captured.out
# __END__

View File

@@ -1,3 +0,0 @@
"""
Unit tests for encryption_handling module
"""

View File

@@ -1,205 +0,0 @@
"""
Unit tests for convert_to_seconds function from timestamp_strings module.
"""
import pytest
from corelibs.datetime_handling.timestamp_convert import convert_to_seconds, TimeParseError, TimeUnitError
class TestConvertToSeconds:
"""Test class for convert_to_seconds function."""
def test_numeric_input_int(self):
"""Test with integer input."""
assert convert_to_seconds(42) == 42
assert convert_to_seconds(0) == 0
assert convert_to_seconds(-5) == -5
def test_numeric_input_float(self):
"""Test with float input."""
assert convert_to_seconds(42.7) == 43 # rounds to 43
assert convert_to_seconds(42.3) == 42 # rounds to 42
assert convert_to_seconds(42.5) == 42 # rounds to 42 (banker's rounding)
assert convert_to_seconds(0.0) == 0
assert convert_to_seconds(-5.7) == -6
def test_numeric_string_input(self):
"""Test with numeric string input."""
assert convert_to_seconds("42") == 42
assert convert_to_seconds("42.7") == 43
assert convert_to_seconds("42.3") == 42
assert convert_to_seconds("0") == 0
assert convert_to_seconds("-5.7") == -6
def test_single_unit_seconds(self):
"""Test with seconds unit."""
assert convert_to_seconds("30s") == 30
assert convert_to_seconds("1s") == 1
assert convert_to_seconds("0s") == 0
def test_single_unit_minutes(self):
"""Test with minutes unit."""
assert convert_to_seconds("5m") == 300 # 5 * 60
assert convert_to_seconds("1m") == 60
assert convert_to_seconds("0m") == 0
def test_single_unit_hours(self):
"""Test with hours unit."""
assert convert_to_seconds("2h") == 7200 # 2 * 3600
assert convert_to_seconds("1h") == 3600
assert convert_to_seconds("0h") == 0
def test_single_unit_days(self):
"""Test with days unit."""
assert convert_to_seconds("1d") == 86400 # 1 * 86400
assert convert_to_seconds("2d") == 172800 # 2 * 86400
assert convert_to_seconds("0d") == 0
def test_single_unit_months(self):
"""Test with months unit (30 days * 12 = 1 year)."""
# Note: The code has M: 2592000 * 12 which is 1 year, not 1 month
# This seems like a bug in the original code, but testing what it actually does
assert convert_to_seconds("1M") == 31104000 # 2592000 * 12
assert convert_to_seconds("2M") == 62208000 # 2 * 2592000 * 12
def test_single_unit_years(self):
"""Test with years unit."""
assert convert_to_seconds("1Y") == 31536000 # 365 * 86400
assert convert_to_seconds("2Y") == 63072000 # 2 * 365 * 86400
def test_long_unit_names(self):
"""Test with long unit names."""
assert convert_to_seconds("1year") == 31536000
assert convert_to_seconds("2years") == 63072000
assert convert_to_seconds("1month") == 31104000
assert convert_to_seconds("2months") == 62208000
assert convert_to_seconds("1day") == 86400
assert convert_to_seconds("2days") == 172800
assert convert_to_seconds("1hour") == 3600
assert convert_to_seconds("2hours") == 7200
assert convert_to_seconds("1minute") == 60
assert convert_to_seconds("2minutes") == 120
assert convert_to_seconds("30min") == 1800
assert convert_to_seconds("1second") == 1
assert convert_to_seconds("2seconds") == 2
assert convert_to_seconds("30sec") == 30
def test_multiple_units(self):
"""Test with multiple units combined."""
assert convert_to_seconds("1h30m") == 5400 # 3600 + 1800
assert convert_to_seconds("1d2h") == 93600 # 86400 + 7200
assert convert_to_seconds("1h30m45s") == 5445 # 3600 + 1800 + 45
assert convert_to_seconds("2d3h4m5s") == 183845 # 172800 + 10800 + 240 + 5
def test_multiple_units_with_spaces(self):
"""Test with multiple units and spaces."""
assert convert_to_seconds("1h 30m") == 5400
assert convert_to_seconds("1d 2h") == 93600
assert convert_to_seconds("1h 30m 45s") == 5445
assert convert_to_seconds("2d 3h 4m 5s") == 183845
def test_mixed_unit_formats(self):
"""Test with mixed short and long unit names."""
assert convert_to_seconds("1hour 30min") == 5400
assert convert_to_seconds("1day 2hours") == 93600
assert convert_to_seconds("1h 30minutes 45sec") == 5445
def test_negative_values(self):
"""Test with negative time strings."""
assert convert_to_seconds("-30s") == -30
assert convert_to_seconds("-1h") == -3600
assert convert_to_seconds("-1h30m") == -5400
assert convert_to_seconds("-2d3h4m5s") == -183845
def test_case_insensitive_long_names(self):
"""Test that long unit names are case insensitive."""
assert convert_to_seconds("1Hour") == 3600
assert convert_to_seconds("1MINUTE") == 60
assert convert_to_seconds("1Day") == 86400
assert convert_to_seconds("2YEARS") == 63072000
def test_duplicate_units_error(self):
"""Test that duplicate units raise TimeParseError."""
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
convert_to_seconds("1h2h")
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
convert_to_seconds("30s45s")
with pytest.raises(TimeParseError, match="Unit 'm' appears more than once"):
convert_to_seconds("1m30m")
def test_invalid_units_error(self):
"""Test that invalid units raise TimeUnitError."""
with pytest.raises(TimeUnitError, match="Unit 'x' is not a valid unit name"):
convert_to_seconds("30x")
with pytest.raises(TimeUnitError, match="Unit 'invalid' is not a valid unit name"):
convert_to_seconds("1invalid")
with pytest.raises(TimeUnitError, match="Unit 'z' is not a valid unit name"):
convert_to_seconds("1h30z")
def test_empty_string(self):
"""Test with empty string."""
assert convert_to_seconds("") == 0
def test_no_matches(self):
"""Test with string that has no time units."""
assert convert_to_seconds("hello") == 0
assert convert_to_seconds("no time here") == 0
def test_zero_values(self):
"""Test with zero values for different units."""
assert convert_to_seconds("0s") == 0
assert convert_to_seconds("0m") == 0
assert convert_to_seconds("0h") == 0
assert convert_to_seconds("0d") == 0
assert convert_to_seconds("0h0m0s") == 0
def test_large_values(self):
"""Test with large time values."""
assert convert_to_seconds("999d") == 86313600 # 999 * 86400
assert convert_to_seconds("100Y") == 3153600000 # 100 * 31536000
def test_order_independence(self):
"""Test that order of units doesn't matter."""
assert convert_to_seconds("30m1h") == 5400 # same as 1h30m
assert convert_to_seconds("45s30m1h") == 5445 # same as 1h30m45s
assert convert_to_seconds("5s4m3h2d") == 183845 # same as 2d3h4m5s
def test_whitespace_handling(self):
"""Test various whitespace scenarios."""
assert convert_to_seconds("1 h") == 3600
assert convert_to_seconds("1h 30m") == 5400
assert convert_to_seconds(" 1h30m ") == 5400
assert convert_to_seconds("1h\t30m") == 5400
def test_mixed_case_short_units(self):
"""Test that short units work with different cases."""
# Note: The regex only matches [a-zA-Z]+ so case matters for the lookup
with pytest.raises(TimeUnitError, match="Unit 'H' is not a valid unit name"):
convert_to_seconds("1H") # 'H' is not in unit_factors, raises error
assert convert_to_seconds("1h") == 3600 # lowercase works
def test_boundary_conditions(self):
"""Test boundary conditions and edge cases."""
# Test with leading zeros
assert convert_to_seconds("01h") == 3600
assert convert_to_seconds("001m") == 60
# Test very small values
assert convert_to_seconds("1s") == 1
def test_negative_with_multiple_units(self):
"""Test negative values with multiple units."""
assert convert_to_seconds("-1h30m45s") == -5445
assert convert_to_seconds("-2d3h") == -183600
def test_duplicate_with_long_names(self):
"""Test duplicate detection with long unit names."""
with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"):
convert_to_seconds("1hour2h") # both resolve to 'h'
with pytest.raises(TimeParseError, match="Unit 's' appears more than once"):
convert_to_seconds("1second30sec") # both resolve to 's'

View File

@@ -1,690 +0,0 @@
"""
PyTest: datetime_handling/datetime_helpers
"""
from datetime import datetime, time
from zoneinfo import ZoneInfo
import pytest
from corelibs.datetime_handling.datetime_helpers import (
create_time,
get_system_timezone,
parse_timezone_data,
get_datetime_iso8601,
validate_date,
parse_flexible_date,
compare_dates,
find_newest_datetime_in_list,
parse_day_of_week_range,
parse_time_range,
times_overlap_or_connect,
is_time_in_range,
reorder_weekdays_from_today,
DAYS_OF_WEEK_LONG_TO_SHORT,
DAYS_OF_WEEK_ISO,
DAYS_OF_WEEK_ISO_REVERSED,
)
class TestConstants:
"""Test suite for module constants"""
def test_days_of_week_long_to_short(self):
"""Test DAYS_OF_WEEK_LONG_TO_SHORT dictionary"""
assert DAYS_OF_WEEK_LONG_TO_SHORT['Monday'] == 'Mon'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Tuesday'] == 'Tue'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Friday'] == 'Fri'
assert DAYS_OF_WEEK_LONG_TO_SHORT['Sunday'] == 'Sun'
assert len(DAYS_OF_WEEK_LONG_TO_SHORT) == 7
def test_days_of_week_iso(self):
"""Test DAYS_OF_WEEK_ISO dictionary"""
assert DAYS_OF_WEEK_ISO[1] == 'Mon'
assert DAYS_OF_WEEK_ISO[5] == 'Fri'
assert DAYS_OF_WEEK_ISO[7] == 'Sun'
assert len(DAYS_OF_WEEK_ISO) == 7
def test_days_of_week_iso_reversed(self):
"""Test DAYS_OF_WEEK_ISO_REVERSED dictionary"""
assert DAYS_OF_WEEK_ISO_REVERSED['Mon'] == 1
assert DAYS_OF_WEEK_ISO_REVERSED['Fri'] == 5
assert DAYS_OF_WEEK_ISO_REVERSED['Sun'] == 7
assert len(DAYS_OF_WEEK_ISO_REVERSED) == 7
class TestCreateTime:
"""Test suite for create_time function"""
def test_create_time_default_format(self):
"""Test create_time with default format"""
timestamp = 1609459200.0 # 2021-01-01 00:00:00 UTC
result = create_time(timestamp)
# Result depends on system timezone, so just check format
assert len(result) == 19
assert '-' in result
assert ':' in result
def test_create_time_custom_format(self):
"""Test create_time with custom format"""
timestamp = 1609459200.0
result = create_time(timestamp, "%Y/%m/%d")
# Check basic format structure
assert '/' in result
assert len(result) == 10
def test_create_time_with_microseconds(self):
"""Test create_time with microseconds in format"""
timestamp = 1609459200.123456
result = create_time(timestamp, "%Y-%m-%d %H:%M:%S")
assert len(result) == 19
class TestGetSystemTimezone:
"""Test suite for get_system_timezone function"""
def test_get_system_timezone_returns_tuple(self):
"""Test that get_system_timezone returns a tuple"""
result = get_system_timezone()
assert isinstance(result, tuple)
assert len(result) == 2
def test_get_system_timezone_returns_valid_data(self):
"""Test that get_system_timezone returns valid timezone info"""
system_tz, timezone_name = get_system_timezone()
assert system_tz is not None
assert isinstance(timezone_name, str)
assert len(timezone_name) > 0
class TestParseTimezoneData:
"""Test suite for parse_timezone_data function"""
def test_parse_timezone_data_valid_timezone(self):
"""Test parse_timezone_data with valid timezone string"""
result = parse_timezone_data('Asia/Tokyo')
assert isinstance(result, ZoneInfo)
assert str(result) == 'Asia/Tokyo'
def test_parse_timezone_data_utc(self):
"""Test parse_timezone_data with UTC"""
result = parse_timezone_data('UTC')
assert isinstance(result, ZoneInfo)
assert str(result) == 'UTC'
def test_parse_timezone_data_empty_string(self):
"""Test parse_timezone_data with empty string falls back to system timezone"""
result = parse_timezone_data('')
assert isinstance(result, ZoneInfo)
def test_parse_timezone_data_invalid_timezone(self):
"""Test parse_timezone_data with invalid timezone falls back to system timezone"""
# Invalid timezones fall back to system timezone or UTC
result = parse_timezone_data('Invalid/Timezone')
assert isinstance(result, ZoneInfo)
# Should be either system timezone or UTC
def test_parse_timezone_data_none(self):
"""Test parse_timezone_data with None falls back to system timezone"""
result = parse_timezone_data()
assert isinstance(result, ZoneInfo)
def test_parse_timezone_data_various_timezones(self):
"""Test parse_timezone_data with various timezone strings"""
timezones = ['America/New_York', 'Europe/London', 'Asia/Seoul']
for tz in timezones:
result = parse_timezone_data(tz)
assert isinstance(result, ZoneInfo)
assert str(result) == tz
class TestGetDatetimeIso8601:
"""Test suite for get_datetime_iso8601 function"""
def test_get_datetime_iso8601_default_params(self):
"""Test get_datetime_iso8601 with default parameters"""
result = get_datetime_iso8601()
# Should be in ISO 8601 format with T separator and microseconds
assert 'T' in result
assert '.' in result # microseconds
# Check basic ISO 8601 format
datetime.fromisoformat(result) # Should not raise
def test_get_datetime_iso8601_custom_timezone_string(self):
"""Test get_datetime_iso8601 with custom timezone string"""
result = get_datetime_iso8601('UTC')
assert '+00:00' in result or 'Z' in result or result.endswith('+00:00')
def test_get_datetime_iso8601_custom_timezone_zoneinfo(self):
"""Test get_datetime_iso8601 with ZoneInfo object"""
tz = ZoneInfo('Asia/Tokyo')
result = get_datetime_iso8601(tz)
assert 'T' in result
datetime.fromisoformat(result) # Should not raise
def test_get_datetime_iso8601_custom_separator(self):
"""Test get_datetime_iso8601 with custom separator"""
result = get_datetime_iso8601(sep=' ')
assert ' ' in result
assert 'T' not in result
def test_get_datetime_iso8601_different_timespec(self):
"""Test get_datetime_iso8601 with different timespec values"""
result_seconds = get_datetime_iso8601(timespec='seconds')
assert '.' not in result_seconds # No microseconds
result_milliseconds = get_datetime_iso8601(timespec='milliseconds')
# Should have milliseconds (3 digits after decimal)
assert '.' in result_milliseconds
class TestValidateDate:
"""Test suite for validate_date function"""
def test_validate_date_valid_hyphen_format(self):
"""Test validate_date with valid Y-m-d format"""
assert validate_date('2023-12-25') is True
assert validate_date('2024-01-01') is True
def test_validate_date_valid_slash_format(self):
"""Test validate_date with valid Y/m/d format"""
assert validate_date('2023/12/25') is True
assert validate_date('2024/01/01') is True
def test_validate_date_invalid_format(self):
"""Test validate_date with invalid format"""
assert validate_date('25-12-2023') is False
assert validate_date('2023.12.25') is False
assert validate_date('invalid') is False
def test_validate_date_invalid_date(self):
"""Test validate_date with invalid date values"""
assert validate_date('2023-13-01') is False # Invalid month
assert validate_date('2023-02-30') is False # Invalid day
def test_validate_date_with_not_before(self):
"""Test validate_date with not_before constraint"""
not_before = datetime(2023, 12, 1)
assert validate_date('2023-12-25', not_before=not_before) is True
assert validate_date('2023-11-25', not_before=not_before) is False
def test_validate_date_with_not_after(self):
"""Test validate_date with not_after constraint"""
not_after = datetime(2023, 12, 31)
assert validate_date('2023-12-25', not_after=not_after) is True
assert validate_date('2024-01-01', not_after=not_after) is False
def test_validate_date_with_both_constraints(self):
"""Test validate_date with both not_before and not_after constraints"""
not_before = datetime(2023, 12, 1)
not_after = datetime(2023, 12, 31)
assert validate_date('2023-12-15', not_before=not_before, not_after=not_after) is True
assert validate_date('2023-11-30', not_before=not_before, not_after=not_after) is False
assert validate_date('2024-01-01', not_before=not_before, not_after=not_after) is False
class TestParseFlexibleDate:
"""Test suite for parse_flexible_date function"""
def test_parse_flexible_date_iso8601_full(self):
"""Test parse_flexible_date with full ISO 8601 format"""
result = parse_flexible_date('2023-12-25T15:30:45')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
assert result.hour == 15
assert result.minute == 30
assert result.second == 45
def test_parse_flexible_date_iso8601_with_microseconds(self):
"""Test parse_flexible_date with microseconds"""
result = parse_flexible_date('2023-12-25T15:30:45.123456')
assert isinstance(result, datetime)
assert result.microsecond == 123456
def test_parse_flexible_date_simple_date(self):
"""Test parse_flexible_date with simple date format"""
result = parse_flexible_date('2023-12-25')
assert isinstance(result, datetime)
assert result.year == 2023
assert result.month == 12
assert result.day == 25
def test_parse_flexible_date_with_timezone_string(self):
"""Test parse_flexible_date with timezone string"""
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='Asia/Tokyo')
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_with_timezone_zoneinfo(self):
"""Test parse_flexible_date with ZoneInfo object"""
tz = ZoneInfo('UTC')
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz=tz)
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_with_timezone_no_shift(self):
"""Test parse_flexible_date with timezone but no shift"""
result = parse_flexible_date('2023-12-25T15:30:45', timezone_tz='UTC', shift_time_zone=False)
assert isinstance(result, datetime)
assert result.hour == 15 # Should not shift
def test_parse_flexible_date_with_timezone_shift(self):
"""Test parse_flexible_date with timezone shift"""
result = parse_flexible_date('2023-12-25T15:30:45+00:00', timezone_tz='Asia/Tokyo', shift_time_zone=True)
assert isinstance(result, datetime)
assert result.tzinfo is not None
def test_parse_flexible_date_invalid_format(self):
"""Test parse_flexible_date with invalid format returns None"""
result = parse_flexible_date('invalid-date')
assert result is None
def test_parse_flexible_date_whitespace(self):
"""Test parse_flexible_date with whitespace"""
result = parse_flexible_date(' 2023-12-25 ')
assert isinstance(result, datetime)
assert result.year == 2023
class TestCompareDates:
"""Test suite for compare_dates function"""
def test_compare_dates_first_newer(self):
"""Test compare_dates when first date is newer"""
result = compare_dates('2024-01-02', '2024-01-01')
assert result is True
def test_compare_dates_first_older(self):
"""Test compare_dates when first date is older"""
result = compare_dates('2024-01-01', '2024-01-02')
assert result is False
def test_compare_dates_equal(self):
"""Test compare_dates when dates are equal"""
result = compare_dates('2024-01-01', '2024-01-01')
assert result is False
def test_compare_dates_with_time(self):
"""Test compare_dates with time components (should only compare dates)"""
result = compare_dates('2024-01-02T10:00:00', '2024-01-01T23:59:59')
assert result is True
def test_compare_dates_invalid_first_date(self):
"""Test compare_dates with invalid first date"""
result = compare_dates('invalid', '2024-01-01')
assert result is None
def test_compare_dates_invalid_second_date(self):
"""Test compare_dates with invalid second date"""
result = compare_dates('2024-01-01', 'invalid')
assert result is None
def test_compare_dates_both_invalid(self):
"""Test compare_dates with both dates invalid"""
result = compare_dates('invalid1', 'invalid2')
assert result is None
class TestFindNewestDatetimeInList:
"""Test suite for find_newest_datetime_in_list function"""
def test_find_newest_datetime_in_list_basic(self):
"""Test find_newest_datetime_in_list with basic list"""
dates = [
'2023-12-25T10:00:00',
'2024-01-01T12:00:00',
'2023-11-15T08:00:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_with_timezone(self):
"""Test find_newest_datetime_in_list with timezone-aware dates"""
dates = [
'2025-08-06T16:17:39.747+09:00',
'2025-08-05T16:17:39.747+09:00',
'2025-08-07T16:17:39.747+09:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2025-08-07T16:17:39.747+09:00'
def test_find_newest_datetime_in_list_empty_list(self):
"""Test find_newest_datetime_in_list with empty list"""
result = find_newest_datetime_in_list([])
assert result is None
def test_find_newest_datetime_in_list_single_date(self):
"""Test find_newest_datetime_in_list with single date"""
dates = ['2024-01-01T12:00:00']
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_with_invalid_dates(self):
"""Test find_newest_datetime_in_list with some invalid dates"""
dates = [
'2023-12-25T10:00:00',
'invalid-date',
'2024-01-01T12:00:00'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
def test_find_newest_datetime_in_list_all_invalid(self):
"""Test find_newest_datetime_in_list with all invalid dates"""
dates = ['invalid1', 'invalid2', 'invalid3']
result = find_newest_datetime_in_list(dates)
assert result is None
def test_find_newest_datetime_in_list_mixed_formats(self):
"""Test find_newest_datetime_in_list with mixed date formats"""
dates = [
'2023-12-25',
'2024-01-01T12:00:00',
'2023-11-15T08:00:00.123456'
]
result = find_newest_datetime_in_list(dates)
assert result == '2024-01-01T12:00:00'
class TestParseDayOfWeekRange:
"""Test suite for parse_day_of_week_range function"""
def test_parse_day_of_week_range_single_day(self):
"""Test parse_day_of_week_range with single day"""
result = parse_day_of_week_range('Mon')
assert result == [(1, 'Mon')]
def test_parse_day_of_week_range_multiple_days(self):
"""Test parse_day_of_week_range with multiple days"""
result = parse_day_of_week_range('Mon,Wed,Fri')
assert len(result) == 3
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (5, 'Fri') in result
def test_parse_day_of_week_range_simple_range(self):
"""Test parse_day_of_week_range with simple range"""
result = parse_day_of_week_range('Mon-Fri')
assert len(result) == 5
assert result[0] == (1, 'Mon')
assert result[-1] == (5, 'Fri')
def test_parse_day_of_week_range_weekend_spanning(self):
"""Test parse_day_of_week_range with weekend-spanning range"""
result = parse_day_of_week_range('Fri-Mon')
assert len(result) == 4
assert (5, 'Fri') in result
assert (6, 'Sat') in result
assert (7, 'Sun') in result
assert (1, 'Mon') in result
def test_parse_day_of_week_range_long_names(self):
"""Test parse_day_of_week_range with long day names - only works in ranges"""
# Long names only work in ranges, not as standalone days
# This is a limitation of the current implementation
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('Monday,Wednesday')
assert 'Invalid day of week entry found' in str(exc_info.value)
def test_parse_day_of_week_range_mixed_format(self):
"""Test parse_day_of_week_range with short names and ranges"""
result = parse_day_of_week_range('Mon,Wed-Fri')
assert len(result) == 4
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (4, 'Thu') in result
assert (5, 'Fri') in result
def test_parse_day_of_week_range_invalid_day(self):
"""Test parse_day_of_week_range with invalid day"""
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('InvalidDay')
assert 'Invalid day of week entry found' in str(exc_info.value)
def test_parse_day_of_week_range_duplicate_days(self):
"""Test parse_day_of_week_range with duplicate days"""
with pytest.raises(ValueError) as exc_info:
parse_day_of_week_range('Mon,Mon')
assert 'Duplicate day of week entries found' in str(exc_info.value)
def test_parse_day_of_week_range_whitespace_handling(self):
"""Test parse_day_of_week_range with extra whitespace"""
result = parse_day_of_week_range(' Mon , Wed , Fri ')
assert len(result) == 3
assert (1, 'Mon') in result
class TestParseTimeRange:
"""Test suite for parse_time_range function"""
def test_parse_time_range_valid(self):
"""Test parse_time_range with valid time range"""
start, end = parse_time_range('09:00-17:00')
assert start == time(9, 0)
assert end == time(17, 0)
def test_parse_time_range_different_times(self):
"""Test parse_time_range with different time values"""
start, end = parse_time_range('08:30-12:45')
assert start == time(8, 30)
assert end == time(12, 45)
def test_parse_time_range_invalid_block(self):
"""Test parse_time_range with invalid block format"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('09:00')
assert 'Invalid time block' in str(exc_info.value)
def test_parse_time_range_invalid_format(self):
"""Test parse_time_range with invalid time format"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('25:00-26:00')
assert 'Invalid time block format' in str(exc_info.value)
def test_parse_time_range_start_after_end(self):
"""Test parse_time_range with start time after end time"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('17:00-09:00')
assert 'start time after end time' in str(exc_info.value)
def test_parse_time_range_equal_times(self):
"""Test parse_time_range with equal start and end times"""
with pytest.raises(ValueError) as exc_info:
parse_time_range('09:00-09:00')
assert 'start time after end time or equal' in str(exc_info.value)
def test_parse_time_range_custom_format(self):
"""Test parse_time_range with custom time format"""
start, end = parse_time_range('09:00:00-17:00:00', time_format='%H:%M:%S')
assert start == time(9, 0, 0)
assert end == time(17, 0, 0)
def test_parse_time_range_whitespace(self):
"""Test parse_time_range with whitespace"""
start, end = parse_time_range(' 09:00-17:00 ')
assert start == time(9, 0)
assert end == time(17, 0)
class TestTimesOverlapOrConnect:
"""Test suite for times_overlap_or_connect function"""
def test_times_overlap_or_connect_clear_overlap(self):
"""Test times_overlap_or_connect with clear overlap"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(10, 0), time(14, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_no_overlap(self):
"""Test times_overlap_or_connect with no overlap"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(13, 0), time(17, 0))
assert times_overlap_or_connect(time1, time2) is False
def test_times_overlap_or_connect_touching_not_allowed(self):
"""Test times_overlap_or_connect with touching ranges (not allowed)"""
time1 = (time(8, 0), time(10, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2, allow_touching=False) is True
def test_times_overlap_or_connect_touching_allowed(self):
"""Test times_overlap_or_connect with touching ranges (allowed)"""
time1 = (time(8, 0), time(10, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2, allow_touching=True) is False
def test_times_overlap_or_connect_one_contains_other(self):
"""Test times_overlap_or_connect when one range contains the other"""
time1 = (time(9, 0), time(17, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_same_start(self):
"""Test times_overlap_or_connect with same start time"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(9, 0), time(14, 0))
assert times_overlap_or_connect(time1, time2) is True
def test_times_overlap_or_connect_same_end(self):
"""Test times_overlap_or_connect with same end time"""
time1 = (time(9, 0), time(12, 0))
time2 = (time(10, 0), time(12, 0))
assert times_overlap_or_connect(time1, time2) is True
class TestIsTimeInRange:
"""Test suite for is_time_in_range function"""
def test_is_time_in_range_within_range(self):
"""Test is_time_in_range with time within range"""
assert is_time_in_range('10:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_at_start(self):
"""Test is_time_in_range with time at start of range"""
assert is_time_in_range('09:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_at_end(self):
"""Test is_time_in_range with time at end of range"""
assert is_time_in_range('17:00:00', '09:00:00', '17:00:00') is True
def test_is_time_in_range_before_range(self):
"""Test is_time_in_range with time before range"""
assert is_time_in_range('08:00:00', '09:00:00', '17:00:00') is False
def test_is_time_in_range_after_range(self):
"""Test is_time_in_range with time after range"""
assert is_time_in_range('18:00:00', '09:00:00', '17:00:00') is False
def test_is_time_in_range_crosses_midnight(self):
"""Test is_time_in_range with range crossing midnight"""
# Range from 22:00 to 06:00
assert is_time_in_range('23:00:00', '22:00:00', '06:00:00') is True
assert is_time_in_range('03:00:00', '22:00:00', '06:00:00') is True
assert is_time_in_range('12:00:00', '22:00:00', '06:00:00') is False
def test_is_time_in_range_midnight_boundary(self):
"""Test is_time_in_range at midnight"""
assert is_time_in_range('00:00:00', '22:00:00', '06:00:00') is True
class TestReorderWeekdaysFromToday:
"""Test suite for reorder_weekdays_from_today function"""
def test_reorder_weekdays_from_monday(self):
"""Test reorder_weekdays_from_today starting from Monday"""
result = reorder_weekdays_from_today('Mon')
values = list(result.values())
assert values[0] == 'Mon'
assert values[-1] == 'Sun'
assert len(result) == 7
def test_reorder_weekdays_from_wednesday(self):
"""Test reorder_weekdays_from_today starting from Wednesday"""
result = reorder_weekdays_from_today('Wed')
values = list(result.values())
assert values[0] == 'Wed'
assert values[1] == 'Thu'
assert values[-1] == 'Tue'
def test_reorder_weekdays_from_sunday(self):
"""Test reorder_weekdays_from_today starting from Sunday"""
result = reorder_weekdays_from_today('Sun')
values = list(result.values())
assert values[0] == 'Sun'
assert values[-1] == 'Sat'
def test_reorder_weekdays_from_long_name(self):
"""Test reorder_weekdays_from_today with long day name"""
result = reorder_weekdays_from_today('Friday')
values = list(result.values())
assert values[0] == 'Fri'
assert values[-1] == 'Thu'
def test_reorder_weekdays_invalid_day(self):
"""Test reorder_weekdays_from_today with invalid day name"""
with pytest.raises(ValueError) as exc_info:
reorder_weekdays_from_today('InvalidDay')
assert 'Invalid day name provided' in str(exc_info.value)
def test_reorder_weekdays_preserves_all_days(self):
"""Test that reorder_weekdays_from_today preserves all 7 days"""
for day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']:
result = reorder_weekdays_from_today(day)
assert len(result) == 7
assert set(result.values()) == {'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'}
class TestEdgeCases:
"""Test suite for edge cases and integration scenarios"""
def test_parse_flexible_date_with_various_iso_formats(self):
"""Test parse_flexible_date handles various ISO format variations"""
formats = [
'2023-12-25',
'2023-12-25T15:30:45',
'2023-12-25T15:30:45.123456',
]
for date_str in formats:
result = parse_flexible_date(date_str)
assert result is not None
assert isinstance(result, datetime)
def test_timezone_consistency_across_functions(self):
"""Test timezone handling consistency across functions"""
tz_str = 'Asia/Tokyo'
tz_obj = parse_timezone_data(tz_str)
# Both should work with get_datetime_iso8601
result1 = get_datetime_iso8601(tz_str)
result2 = get_datetime_iso8601(tz_obj)
assert result1 is not None
assert result2 is not None
def test_date_validation_and_parsing_consistency(self):
"""Test that validate_date and parse_flexible_date agree"""
valid_dates = ['2023-12-25', '2024/01/01']
for date_str in valid_dates:
# normalize format for parse_flexible_date
normalized = date_str.replace('/', '-')
assert validate_date(date_str) is True
assert parse_flexible_date(normalized) is not None
def test_day_of_week_range_complex_scenario(self):
"""Test parse_day_of_week_range with complex mixed input"""
result = parse_day_of_week_range('Mon,Wed-Fri,Sun')
assert len(result) == 5
assert (1, 'Mon') in result
assert (3, 'Wed') in result
assert (4, 'Thu') in result
assert (5, 'Fri') in result
assert (7, 'Sun') in result
def test_time_range_boundary_conditions(self):
"""Test parse_time_range with boundary times"""
start, end = parse_time_range('00:00-23:59')
assert start == time(0, 0)
assert end == time(23, 59)
# __END__

View File

@@ -1,462 +0,0 @@
"""
PyTest: datetime_handling/timestamp_convert - seconds_to_string and convert_timestamp functions
"""
from corelibs.datetime_handling.timestamp_convert import seconds_to_string, convert_timestamp
class TestSecondsToString:
"""Test suite for seconds_to_string function"""
def test_basic_integer_seconds(self):
"""Test conversion of basic integer seconds"""
assert seconds_to_string(0) == "0s"
assert seconds_to_string(1) == "1s"
assert seconds_to_string(30) == "30s"
assert seconds_to_string(59) == "59s"
def test_minutes_conversion(self):
"""Test conversion involving minutes"""
assert seconds_to_string(60) == "1m"
assert seconds_to_string(90) == "1m 30s"
assert seconds_to_string(120) == "2m"
assert seconds_to_string(3599) == "59m 59s"
def test_hours_conversion(self):
"""Test conversion involving hours"""
assert seconds_to_string(3600) == "1h"
assert seconds_to_string(3660) == "1h 1m"
assert seconds_to_string(3661) == "1h 1m 1s"
assert seconds_to_string(7200) == "2h"
assert seconds_to_string(7260) == "2h 1m"
def test_days_conversion(self):
"""Test conversion involving days"""
assert seconds_to_string(86400) == "1d"
assert seconds_to_string(86401) == "1d 1s"
assert seconds_to_string(90000) == "1d 1h"
assert seconds_to_string(90061) == "1d 1h 1m 1s"
assert seconds_to_string(172800) == "2d"
def test_complex_combinations(self):
"""Test complex time combinations"""
# 1 day, 2 hours, 3 minutes, 4 seconds
total = 86400 + 7200 + 180 + 4
assert seconds_to_string(total) == "1d 2h 3m 4s"
# 5 days, 23 hours, 59 minutes, 59 seconds
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
assert seconds_to_string(total) == "5d 23h 59m 59s"
def test_fractional_seconds_default_precision(self):
"""Test fractional seconds with default precision (3 decimal places)"""
assert seconds_to_string(0.1) == "0.1s"
assert seconds_to_string(0.123) == "0.123s"
assert seconds_to_string(0.1234) == "0.123s"
assert seconds_to_string(1.5) == "1.5s"
assert seconds_to_string(1.567) == "1.567s"
assert seconds_to_string(1.5678) == "1.568s"
def test_fractional_seconds_microsecond_precision(self):
"""Test fractional seconds with microsecond precision"""
assert seconds_to_string(0.1, show_microseconds=True) == "0.1s"
assert seconds_to_string(0.123456, show_microseconds=True) == "0.123456s"
assert seconds_to_string(0.1234567, show_microseconds=True) == "0.123457s"
assert seconds_to_string(1.5, show_microseconds=True) == "1.5s"
assert seconds_to_string(1.567890, show_microseconds=True) == "1.56789s"
def test_fractional_seconds_with_larger_units(self):
"""Test fractional seconds combined with larger time units"""
# 1 minute and 30.5 seconds
assert seconds_to_string(90.5) == "1m 30.5s"
assert seconds_to_string(90.5, show_microseconds=True) == "1m 30.5s"
# 1 hour, 1 minute, and 1.123 seconds
total = 3600 + 60 + 1.123
assert seconds_to_string(total) == "1h 1m 1.123s"
assert seconds_to_string(total, show_microseconds=True) == "1h 1m 1.123s"
def test_negative_values(self):
"""Test negative time values"""
assert seconds_to_string(-1) == "-1s"
assert seconds_to_string(-60) == "-1m"
assert seconds_to_string(-90) == "-1m 30s"
assert seconds_to_string(-3661) == "-1h 1m 1s"
assert seconds_to_string(-86401) == "-1d 1s"
assert seconds_to_string(-1.5) == "-1.5s"
assert seconds_to_string(-90.123) == "-1m 30.123s"
def test_zero_handling(self):
"""Test various zero values"""
assert seconds_to_string(0) == "0s"
assert seconds_to_string(0.0) == "0s"
assert seconds_to_string(-0) == "0s"
assert seconds_to_string(-0.0) == "0s"
def test_float_input_types(self):
"""Test various float input types"""
assert seconds_to_string(1.0) == "1s"
assert seconds_to_string(60.0) == "1m"
assert seconds_to_string(3600.0) == "1h"
assert seconds_to_string(86400.0) == "1d"
def test_large_values(self):
"""Test handling of large time values"""
# 365 days (1 year)
year_seconds = 365 * 86400
assert seconds_to_string(year_seconds) == "365d"
# 1000 days
assert seconds_to_string(1000 * 86400) == "1000d"
# Large number with all units
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
result = seconds_to_string(large_time)
assert result.startswith("999d")
assert "23h" in result
assert "59m" in result
assert "59.999s" in result
def test_rounding_behavior(self):
"""Test rounding behavior for fractional seconds"""
# Default precision (3 decimal places) - values are truncated via rstrip
assert seconds_to_string(1.0004) == "1s" # Truncates trailing zeros after rstrip
assert seconds_to_string(1.0005) == "1s" # Truncates trailing zeros after rstrip
assert seconds_to_string(1.9999) == "2s" # Rounds up and strips .000
# Microsecond precision (6 decimal places)
assert seconds_to_string(1.0000004, show_microseconds=True) == "1s"
assert seconds_to_string(1.0000005, show_microseconds=True) == "1.000001s"
def test_trailing_zero_removal(self):
"""Test that trailing zeros are properly removed"""
assert seconds_to_string(1.100) == "1.1s"
assert seconds_to_string(1.120) == "1.12s"
assert seconds_to_string(1.123) == "1.123s"
assert seconds_to_string(1.100000, show_microseconds=True) == "1.1s"
assert seconds_to_string(1.123000, show_microseconds=True) == "1.123s"
def test_invalid_input_types(self):
"""Test handling of invalid input types"""
# String inputs should be returned as-is
assert seconds_to_string("invalid") == "invalid"
assert seconds_to_string("not a number") == "not a number"
assert seconds_to_string("") == ""
def test_edge_cases_boundary_values(self):
"""Test edge cases at unit boundaries"""
# Exactly 1 minute - 1 second
assert seconds_to_string(59) == "59s"
assert seconds_to_string(59.999) == "59.999s"
# Exactly 1 hour - 1 second
assert seconds_to_string(3599) == "59m 59s"
assert seconds_to_string(3599.999) == "59m 59.999s"
# Exactly 1 day - 1 second
assert seconds_to_string(86399) == "23h 59m 59s"
assert seconds_to_string(86399.999) == "23h 59m 59.999s"
def test_very_small_fractional_seconds(self):
"""Test very small fractional values"""
assert seconds_to_string(0.001) == "0.001s"
assert seconds_to_string(0.0001) == "0s" # Below default precision
assert seconds_to_string(0.000001, show_microseconds=True) == "0.000001s"
assert seconds_to_string(0.0000001, show_microseconds=True) == "0s" # Below microsecond precision
def test_precision_consistency(self):
"""Test that precision is consistent across different scenarios"""
# With other units present
assert seconds_to_string(61.123456) == "1m 1.123s"
assert seconds_to_string(61.123456, show_microseconds=True) == "1m 1.123456s"
# Large values with fractional seconds
large_val = 90061.123456 # 1d 1h 1m 1.123456s
assert seconds_to_string(large_val) == "1d 1h 1m 1.123s"
assert seconds_to_string(large_val, show_microseconds=True) == "1d 1h 1m 1.123456s"
def test_string_numeric_inputs(self):
"""Test string inputs that represent numbers"""
# String inputs should be returned as-is, even if they look like numbers
assert seconds_to_string("60") == "60"
assert seconds_to_string("1.5") == "1.5"
assert seconds_to_string("0") == "0"
assert seconds_to_string("-60") == "-60"
class TestConvertTimestamp:
"""Test suite for convert_timestamp function"""
def test_basic_integer_seconds(self):
"""Test conversion of basic integer seconds"""
assert convert_timestamp(0) == "0s 0ms"
assert convert_timestamp(1) == "1s 0ms"
assert convert_timestamp(30) == "30s 0ms"
assert convert_timestamp(59) == "59s 0ms"
def test_basic_without_microseconds(self):
"""Test conversion without showing microseconds"""
assert convert_timestamp(0, show_microseconds=False) == "0s"
assert convert_timestamp(1, show_microseconds=False) == "1s"
assert convert_timestamp(30, show_microseconds=False) == "30s"
assert convert_timestamp(59, show_microseconds=False) == "59s"
def test_minutes_conversion(self):
"""Test conversion involving minutes"""
assert convert_timestamp(60) == "1m 0s 0ms"
assert convert_timestamp(90) == "1m 30s 0ms"
assert convert_timestamp(120) == "2m 0s 0ms"
assert convert_timestamp(3599) == "59m 59s 0ms"
def test_minutes_conversion_without_microseconds(self):
"""Test conversion involving minutes without microseconds"""
assert convert_timestamp(60, show_microseconds=False) == "1m 0s"
assert convert_timestamp(90, show_microseconds=False) == "1m 30s"
assert convert_timestamp(120, show_microseconds=False) == "2m 0s"
def test_hours_conversion(self):
"""Test conversion involving hours"""
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
assert convert_timestamp(3660) == "1h 1m 0s 0ms"
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
assert convert_timestamp(7200) == "2h 0m 0s 0ms"
assert convert_timestamp(7260) == "2h 1m 0s 0ms"
def test_hours_conversion_without_microseconds(self):
"""Test conversion involving hours without microseconds"""
assert convert_timestamp(3600, show_microseconds=False) == "1h 0m 0s"
assert convert_timestamp(3660, show_microseconds=False) == "1h 1m 0s"
assert convert_timestamp(3661, show_microseconds=False) == "1h 1m 1s"
def test_days_conversion(self):
"""Test conversion involving days"""
assert convert_timestamp(86400) == "1d 0h 0m 0s 0ms"
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
assert convert_timestamp(90061) == "1d 1h 1m 1s 0ms"
assert convert_timestamp(172800) == "2d 0h 0m 0s 0ms"
def test_days_conversion_without_microseconds(self):
"""Test conversion involving days without microseconds"""
assert convert_timestamp(86400, show_microseconds=False) == "1d 0h 0m 0s"
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
assert convert_timestamp(90000, show_microseconds=False) == "1d 1h 0m 0s"
def test_complex_combinations(self):
"""Test complex time combinations"""
# 1 day, 2 hours, 3 minutes, 4 seconds
total = 86400 + 7200 + 180 + 4
assert convert_timestamp(total) == "1d 2h 3m 4s 0ms"
# 5 days, 23 hours, 59 minutes, 59 seconds
total = 5 * 86400 + 23 * 3600 + 59 * 60 + 59
assert convert_timestamp(total) == "5d 23h 59m 59s 0ms"
def test_fractional_seconds_with_microseconds(self):
"""Test fractional seconds showing microseconds"""
# Note: ms value is the integer of the decimal part string after rounding to 4 places
assert convert_timestamp(0.1) == "0s 1ms" # 0.1 → "0.1" → ms=1
assert convert_timestamp(0.123) == "0s 123ms" # 0.123 → "0.123" → ms=123
assert convert_timestamp(0.1234) == "0s 1234ms" # 0.1234 → "0.1234" → ms=1234
assert convert_timestamp(1.5) == "1s 5ms" # 1.5 → "1.5" → ms=5
assert convert_timestamp(1.567) == "1s 567ms" # 1.567 → "1.567" → ms=567
assert convert_timestamp(1.5678) == "1s 5678ms" # 1.5678 rounds to 1.5678 → ms=5678
def test_fractional_seconds_rounding(self):
"""Test rounding of fractional seconds to 4 decimal places"""
# The function rounds to 4 decimal places before splitting
assert convert_timestamp(0.12345) == "0s 1235ms" # Rounds to 0.1235
assert convert_timestamp(0.123456) == "0s 1235ms" # Rounds to 0.1235
assert convert_timestamp(1.99999) == "2s 0ms" # Rounds to 2.0
def test_fractional_seconds_with_larger_units(self):
"""Test fractional seconds combined with larger time units"""
# 1 minute and 30.5 seconds
assert convert_timestamp(90.5) == "1m 30s 5ms"
# 1 hour, 1 minute, and 1.123 seconds
total = 3600 + 60 + 1.123
assert convert_timestamp(total) == "1h 1m 1s 123ms"
def test_negative_values(self):
"""Test negative time values"""
assert convert_timestamp(-1) == "-1s 0ms"
assert convert_timestamp(-60) == "-1m 0s 0ms"
assert convert_timestamp(-90) == "-1m 30s 0ms"
assert convert_timestamp(-3661) == "-1h 1m 1s 0ms"
assert convert_timestamp(-86401) == "-1d 0h 0m 1s 0ms"
assert convert_timestamp(-1.5) == "-1s 5ms"
assert convert_timestamp(-90.123) == "-1m 30s 123ms"
def test_negative_without_microseconds(self):
"""Test negative values without microseconds"""
assert convert_timestamp(-1, show_microseconds=False) == "-1s"
assert convert_timestamp(-60, show_microseconds=False) == "-1m 0s"
assert convert_timestamp(-90.123, show_microseconds=False) == "-1m 30s"
def test_zero_handling(self):
"""Test various zero values"""
assert convert_timestamp(0) == "0s 0ms"
assert convert_timestamp(0.0) == "0s 0ms"
assert convert_timestamp(-0) == "0s 0ms"
assert convert_timestamp(-0.0) == "0s 0ms"
def test_zero_filling_behavior(self):
"""Test that zeros are filled between set values"""
# If we have days and seconds, hours and minutes should be 0
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
# If we have hours and seconds, minutes should be 0
assert convert_timestamp(3601) == "1h 0m 1s 0ms"
# If we have days and hours, minutes and seconds should be 0
assert convert_timestamp(90000) == "1d 1h 0m 0s 0ms"
def test_milliseconds_display(self):
"""Test milliseconds are always shown when show_microseconds=True"""
# Even with no fractional part, 0ms should be shown
assert convert_timestamp(1) == "1s 0ms"
assert convert_timestamp(60) == "1m 0s 0ms"
assert convert_timestamp(3600) == "1h 0m 0s 0ms"
# With fractional part, ms should be shown
assert convert_timestamp(1.001) == "1s 1ms" # "1.001" → ms=1
assert convert_timestamp(1.0001) == "1s 1ms" # "1.0001" → ms=1
def test_float_input_types(self):
"""Test various float input types"""
assert convert_timestamp(1.0) == "1s 0ms"
assert convert_timestamp(60.0) == "1m 0s 0ms"
assert convert_timestamp(3600.0) == "1h 0m 0s 0ms"
assert convert_timestamp(86400.0) == "1d 0h 0m 0s 0ms"
def test_large_values(self):
"""Test handling of large time values"""
# 365 days (1 year)
year_seconds = 365 * 86400
assert convert_timestamp(year_seconds) == "365d 0h 0m 0s 0ms"
# 1000 days
assert convert_timestamp(1000 * 86400) == "1000d 0h 0m 0s 0ms"
# Large number with all units
large_time = 999 * 86400 + 23 * 3600 + 59 * 60 + 59.999
result = convert_timestamp(large_time)
assert result.startswith("999d")
assert "23h" in result
assert "59m" in result
assert "59s" in result
assert "999ms" in result # 59.999 rounds to 59.999, ms=999
def test_invalid_input_types(self):
"""Test handling of invalid input types"""
# String inputs should be returned as-is
assert convert_timestamp("invalid") == "invalid"
assert convert_timestamp("not a number") == "not a number"
assert convert_timestamp("") == ""
def test_string_numeric_inputs(self):
"""Test string inputs that represent numbers"""
# String inputs should be returned as-is, even if they look like numbers
assert convert_timestamp("60") == "60"
assert convert_timestamp("1.5") == "1.5"
assert convert_timestamp("0") == "0"
assert convert_timestamp("-60") == "-60"
def test_edge_cases_boundary_values(self):
"""Test edge cases at unit boundaries"""
# Exactly 1 minute - 1 second
assert convert_timestamp(59) == "59s 0ms"
assert convert_timestamp(59.999) == "59s 999ms"
# Exactly 1 hour - 1 second
assert convert_timestamp(3599) == "59m 59s 0ms"
assert convert_timestamp(3599.999) == "59m 59s 999ms"
# Exactly 1 day - 1 second
assert convert_timestamp(86399) == "23h 59m 59s 0ms"
assert convert_timestamp(86399.999) == "23h 59m 59s 999ms"
def test_very_small_fractional_seconds(self):
"""Test very small fractional values"""
assert convert_timestamp(0.001) == "0s 1ms" # 0.001 → "0.001" → ms=1
assert convert_timestamp(0.0001) == "0s 1ms" # 0.0001 → "0.0001" → ms=1
assert convert_timestamp(0.00005) == "0s 1ms" # 0.00005 rounds to 0.0001 → ms=1
assert convert_timestamp(0.00004) == "0s 0ms" # 0.00004 rounds to 0.0 → ms=0
def test_milliseconds_extraction(self):
"""Test that milliseconds are correctly extracted from fractional part"""
# The ms value is the integer of the decimal part string, not a conversion
# So 0.1 → "0.1" → ms=1, NOT 100ms as you might expect
assert convert_timestamp(0.1) == "0s 1ms"
# 0.01 seconds → "0.01" → ms=1 (int("01") = 1)
assert convert_timestamp(0.01) == "0s 1ms"
# 0.001 seconds → "0.001" → ms=1
assert convert_timestamp(0.001) == "0s 1ms"
# 0.0001 seconds → "0.0001" → ms=1
assert convert_timestamp(0.0001) == "0s 1ms"
# 0.00004 seconds rounds to "0.0" → ms=0
assert convert_timestamp(0.00004) == "0s 0ms"
def test_comparison_with_seconds_to_string(self):
"""Test differences between convert_timestamp and seconds_to_string"""
# convert_timestamp fills zeros and adds ms
# seconds_to_string omits zeros and no ms
assert convert_timestamp(86401) == "1d 0h 0m 1s 0ms"
assert seconds_to_string(86401) == "1d 1s"
assert convert_timestamp(3661) == "1h 1m 1s 0ms"
assert seconds_to_string(3661) == "1h 1m 1s"
# With microseconds disabled, still different due to zero-filling
assert convert_timestamp(86401, show_microseconds=False) == "1d 0h 0m 1s"
assert seconds_to_string(86401) == "1d 1s"
def test_precision_consistency(self):
"""Test that precision is consistent across different scenarios"""
# With other units present
assert convert_timestamp(61.123456) == "1m 1s 1235ms" # Rounds to 61.1235
# Large values with fractional seconds
large_val = 90061.123456 # 1d 1h 1m 1.123456s
assert convert_timestamp(large_val) == "1d 1h 1m 1s 1235ms" # Rounds to .1235
def test_microseconds_flag_consistency(self):
"""Test that show_microseconds flag works consistently"""
test_values = [0, 1, 60, 3600, 86400, 1.5, 90.123, -60]
for val in test_values:
with_ms = convert_timestamp(val, show_microseconds=True)
without_ms = convert_timestamp(val, show_microseconds=False)
# With microseconds should contain 'ms', without should not
assert "ms" in with_ms
assert "ms" not in without_ms
# Both should start with same sign if negative
if val < 0:
assert with_ms.startswith("-")
assert without_ms.startswith("-")
def test_format_consistency(self):
"""Test that output format is consistent"""
# All outputs should have consistent spacing and unit ordering
# Format should be: [d ]h m s[ ms]
result = convert_timestamp(93784.5678) # 1d 2h 3m 4.5678s
# 93784.5678 rounds to 93784.5678, splits to ["93784", "5678"]
assert result == "1d 2h 3m 4s 5678ms"
# Verify parts are in correct order
parts = result.split()
# Extract units properly: last 1-2 chars that are letters
units: list[str] = []
for p in parts:
if p.endswith('ms'):
units.append('ms')
elif p[-1].isalpha():
units.append(p[-1])
# Should be in order: d, h, m, s, ms
expected_order = ['d', 'h', 'm', 's', 'ms']
assert units == expected_order
# __END__

View File

@@ -1,194 +0,0 @@
"""
PyTest: datetime_handling/timestamp_strings
"""
from datetime import datetime
from unittest.mock import patch
from zoneinfo import ZoneInfo
import pytest
# Assuming the class is in a file called timestamp_strings.py
from corelibs.datetime_handling.timestamp_strings import TimestampStrings
class TestTimestampStrings:
"""Test suite for TimestampStrings class"""
def test_default_initialization(self):
"""Test initialization with default timezone"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.time_zone == 'Asia/Tokyo'
assert ts.timestamp_now == mock_now
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 15:30:45'
assert ts.timestamp_file == '2023-12-25_153045'
def test_custom_timezone_initialization(self):
"""Test initialization with custom timezone"""
custom_tz = 'America/New_York'
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
assert ts.timestamp_now == mock_now
def test_invalid_timezone_raises_error(self):
"""Test that invalid timezone raises ValueError"""
invalid_tz = 'Invalid/Timezone'
with pytest.raises(ValueError) as exc_info:
TimestampStrings(time_zone=invalid_tz)
assert 'Zone could not be loaded [Invalid/Timezone]' in str(exc_info.value)
def test_timestamp_formats(self):
"""Test various timestamp format outputs"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
# Mock both datetime.now() calls
mock_now = datetime(2023, 12, 25, 9, 5, 3)
mock_now_tz = datetime(2023, 12, 25, 23, 5, 3, tzinfo=ZoneInfo('Asia/Tokyo'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
ts = TimestampStrings()
assert ts.today == '2023-12-25'
assert ts.timestamp == '2023-12-25 09:05:03'
assert ts.timestamp_file == '2023-12-25_090503'
assert 'JST' in ts.timestamp_tz or 'Asia/Tokyo' in ts.timestamp_tz
def test_different_timezones_produce_different_results(self):
"""Test that different timezones produce different timestamp_tz values"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 12, 0, 0)
mock_datetime.now.return_value = mock_now
# Create instances with different timezones
ts_tokyo = TimestampStrings(time_zone='Asia/Tokyo')
ts_ny = TimestampStrings(time_zone='America/New_York')
# The timezone-aware timestamps should be different
assert ts_tokyo.time_zone != ts_ny.time_zone
# Note: The actual timestamp_tz values will depend on the mocked datetime
def test_class_default_timezone(self):
"""Test that class default timezone is correctly set"""
assert TimestampStrings.TIME_ZONE == 'Asia/Tokyo'
def test_none_timezone_uses_default(self):
"""Test that passing None for timezone uses class default"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings(time_zone=None)
assert ts.time_zone == 'Asia/Tokyo'
def test_timestamp_file_format_no_colons(self):
"""Test that timestamp_file format doesn't contain colons (safe for filenames)"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ':' not in ts.timestamp_file
assert ' ' not in ts.timestamp_file
assert ts.timestamp_file == '2023-12-25_153045'
def test_multiple_instances_independent(self):
"""Test that multiple instances don't interfere with each other"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
ts1 = TimestampStrings(time_zone='Asia/Tokyo')
ts2 = TimestampStrings(time_zone='Europe/London')
assert ts1.time_zone == 'Asia/Tokyo'
assert ts2.time_zone == 'Europe/London'
assert ts1.time_zone != ts2.time_zone
def test_zoneinfo_called_correctly_with_string(self):
"""Test that ZoneInfo is called with correct timezone when passing string"""
with patch('corelibs.datetime_handling.timestamp_strings.ZoneInfo') as mock_zoneinfo:
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_datetime.now.return_value = mock_now
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
assert ts.time_zone == custom_tz
mock_zoneinfo.assert_called_with(custom_tz)
def test_zoneinfo_object_parameter(self):
"""Test that ZoneInfo objects can be passed directly as timezone parameter"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz]
# Create a ZoneInfo object
custom_tz_obj = ZoneInfo('Europe/Paris')
ts = TimestampStrings(time_zone=custom_tz_obj)
# The time_zone should be the ZoneInfo object itself
assert ts.time_zone_zi is custom_tz_obj
assert isinstance(ts.time_zone_zi, ZoneInfo)
def test_zoneinfo_object_vs_string_equivalence(self):
"""Test that ZoneInfo object and string produce equivalent results"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 15, 30, 45)
mock_now_tz = datetime(2023, 12, 25, 15, 30, 45, tzinfo=ZoneInfo('Europe/Paris'))
mock_datetime.now.side_effect = [mock_now, mock_now_tz, mock_now, mock_now_tz]
# Test with string
ts_string = TimestampStrings(time_zone='Europe/Paris')
# Test with ZoneInfo object
ts_zoneinfo = TimestampStrings(time_zone=ZoneInfo('Europe/Paris'))
# Both should produce the same timestamp formats (though time_zone attributes will differ)
assert ts_string.today == ts_zoneinfo.today
assert ts_string.timestamp == ts_zoneinfo.timestamp
assert ts_string.timestamp_file == ts_zoneinfo.timestamp_file
# The time_zone attributes will be different types but represent the same timezone
assert str(ts_string.time_zone) == 'Europe/Paris'
assert isinstance(ts_zoneinfo.time_zone_zi, ZoneInfo)
def test_edge_case_midnight(self):
"""Test timestamp formatting at midnight"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2023, 12, 25, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.timestamp == '2023-12-25 00:00:00'
assert ts.timestamp_file == '2023-12-25_000000'
def test_edge_case_new_year(self):
"""Test timestamp formatting at new year"""
with patch('corelibs.datetime_handling.timestamp_strings.datetime') as mock_datetime:
mock_now = datetime(2024, 1, 1, 0, 0, 0)
mock_datetime.now.return_value = mock_now
ts = TimestampStrings()
assert ts.today == '2024-01-01'
assert ts.timestamp == '2024-01-01 00:00:00'
# __END__

View File

@@ -0,0 +1,461 @@
"""
PyTest: db_handling/sql_main
Tests for SQLMain class - Main SQL interface wrapper
Note: Pylance warnings about "Redefining name from outer scope" in fixtures are expected.
This is standard pytest fixture behavior where fixture parameters shadow fixture definitions.
"""
# pylint: disable=redefined-outer-name,too-many-public-methods,protected-access
# pyright: reportUnknownParameterType=false, reportUnknownArgumentType=false
# pyright: reportMissingParameterType=false, reportUnknownVariableType=false
# pyright: reportArgumentType=false, reportGeneralTypeIssues=false
from pathlib import Path
from typing import Generator
from unittest.mock import MagicMock, patch
import pytest
from corelibs.db_handling.sql_main import SQLMain, IDENT_SPLIT_CHARACTER
from corelibs.db_handling.sqlite_io import SQLiteIO
# Test fixtures
@pytest.fixture
def mock_logger() -> MagicMock:
"""Create a mock logger for testing"""
logger = MagicMock()
logger.debug = MagicMock()
logger.info = MagicMock()
logger.warning = MagicMock()
logger.error = MagicMock()
return logger
@pytest.fixture
def temp_db_path(tmp_path: Path) -> Path:
"""Create a temporary database file path"""
return tmp_path / "test_database.db"
@pytest.fixture
def mock_sqlite_io() -> Generator[MagicMock, None, None]:
"""Create a mock SQLiteIO instance"""
mock_io = MagicMock(spec=SQLiteIO)
mock_io.conn = MagicMock()
mock_io.db_connected = MagicMock(return_value=True)
mock_io.db_close = MagicMock()
mock_io.execute_query = MagicMock(return_value=[])
yield mock_io
# Test constant
class TestConstants:
"""Tests for module-level constants"""
def test_ident_split_character(self):
"""Test that IDENT_SPLIT_CHARACTER is defined correctly"""
assert IDENT_SPLIT_CHARACTER == ':'
# Test SQLMain class initialization
class TestSQLMainInit:
"""Tests for SQLMain.__init__"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_successful_initialization_sqlite(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test successful initialization with SQLite"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
assert sql_main.log == mock_logger
assert sql_main.dbh == mock_sqlite_instance
assert sql_main.db_target == 'sqlite'
mock_sqlite_class.assert_called_once_with(mock_logger, str(temp_db_path), row_factory='Dict')
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_initialization_connection_failure(self, mock_sqlite_class: MagicMock, mock_logger: MagicMock):
"""Test initialization fails when connection cannot be established"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = None
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = 'sqlite:/path/to/db.db'
with pytest.raises(ValueError, match='DB Connection failed for: sqlite'):
SQLMain(mock_logger, db_ident)
def test_initialization_invalid_db_target(self, mock_logger: MagicMock):
"""Test initialization with unsupported database target"""
db_ident = 'postgresql:/path/to/db'
with pytest.raises(ValueError, match='SQL interface for postgresql is not implemented'):
SQLMain(mock_logger, db_ident)
def test_initialization_malformed_db_ident(self, mock_logger: MagicMock):
"""Test initialization with malformed db_ident string"""
db_ident = 'sqlite_no_colon'
with pytest.raises(ValueError):
SQLMain(mock_logger, db_ident)
# Test SQLMain.connect method
class TestSQLMainConnect:
"""Tests for SQLMain.connect"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_connect_when_already_connected(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test connect warns when already connected"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
# Reset mock to check second call
mock_logger.warning.reset_mock()
# Try to connect again
sql_main.connect(f'sqlite:{temp_db_path}')
# Should have warned about existing connection
mock_logger.warning.assert_called_once()
assert 'already exists' in str(mock_logger.warning.call_args)
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_connect_sqlite_success(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test successful SQLite connection"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
sql_main = SQLMain.__new__(SQLMain)
sql_main.log = mock_logger
sql_main.dbh = None
sql_main.db_target = None
db_ident = f'sqlite:{temp_db_path}'
sql_main.connect(db_ident)
assert sql_main.db_target == 'sqlite'
assert sql_main.dbh == mock_sqlite_instance
mock_sqlite_class.assert_called_once_with(mock_logger, str(temp_db_path), row_factory='Dict')
def test_connect_unsupported_database(self, mock_logger: MagicMock):
"""Test connect with unsupported database type"""
sql_main = SQLMain.__new__(SQLMain)
sql_main.log = mock_logger
sql_main.dbh = None
sql_main.db_target = None
db_ident = 'mysql:/path/to/db'
with pytest.raises(ValueError, match='SQL interface for mysql is not implemented'):
sql_main.connect(db_ident)
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_connect_db_connection_failed(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test connect raises error when DB connection fails"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
mock_sqlite_class.return_value = mock_sqlite_instance
sql_main = SQLMain.__new__(SQLMain)
sql_main.log = mock_logger
sql_main.dbh = None
sql_main.db_target = None
db_ident = f'sqlite:{temp_db_path}'
with pytest.raises(ValueError, match='DB Connection failed for: sqlite'):
sql_main.connect(db_ident)
# Test SQLMain.close method
class TestSQLMainClose:
"""Tests for SQLMain.close"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_close_successful(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test successful database close"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.db_close = MagicMock()
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
sql_main.close()
mock_sqlite_instance.db_close.assert_called_once()
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_close_when_not_connected(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test close when not connected does nothing"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.db_close = MagicMock()
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
# Change db_connected to return False to simulate disconnection
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
sql_main.close()
# Should not raise error and should exit early
assert mock_sqlite_instance.db_close.call_count == 0
def test_close_when_dbh_is_none(self, mock_logger: MagicMock):
"""Test close when dbh is None"""
sql_main = SQLMain.__new__(SQLMain)
sql_main.log = mock_logger
sql_main.dbh = None
sql_main.db_target = 'sqlite'
# Should not raise error
sql_main.close()
# Test SQLMain.connected method
class TestSQLMainConnected:
"""Tests for SQLMain.connected"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_connected_returns_true(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test connected returns True when connected"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
assert sql_main.connected() is True
mock_logger.warning.assert_not_called()
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_connected_returns_false_when_not_connected(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test connected returns False and warns when not connected"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
# Reset warning calls from init
mock_logger.warning.reset_mock()
# Change db_connected to return False to simulate disconnection
mock_sqlite_instance.db_connected = MagicMock(return_value=False)
assert sql_main.connected() is False
mock_logger.warning.assert_called_once()
assert 'No connection' in str(mock_logger.warning.call_args)
def test_connected_returns_false_when_dbh_is_none(self, mock_logger: MagicMock):
"""Test connected returns False when dbh is None"""
sql_main = SQLMain.__new__(SQLMain)
sql_main.log = mock_logger
sql_main.dbh = None
sql_main.db_target = 'sqlite'
assert sql_main.connected() is False
mock_logger.warning.assert_called_once()
# Test SQLMain.process_query method
class TestSQLMainProcessQuery:
"""Tests for SQLMain.process_query"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_process_query_success_no_params(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test successful query execution without parameters"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
expected_result = [{'id': 1, 'name': 'test'}]
mock_sqlite_instance.execute_query = MagicMock(return_value=expected_result)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
query = "SELECT * FROM test"
result = sql_main.process_query(query)
assert result == expected_result
mock_sqlite_instance.execute_query.assert_called_once_with(query, None)
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_process_query_success_with_params(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test successful query execution with parameters"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
expected_result = [{'id': 1, 'name': 'test'}]
mock_sqlite_instance.execute_query = MagicMock(return_value=expected_result)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
query = "SELECT * FROM test WHERE id = ?"
params = (1,)
result = sql_main.process_query(query, params)
assert result == expected_result
mock_sqlite_instance.execute_query.assert_called_once_with(query, params)
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_process_query_returns_false_on_error(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test query returns False when execute_query fails"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.execute_query = MagicMock(return_value=False)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
query = "SELECT * FROM nonexistent"
result = sql_main.process_query(query)
assert result is False
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_process_query_dbh_is_none(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test query returns False when dbh is None"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
# Manually set dbh to None
sql_main.dbh = None
query = "SELECT * FROM test"
result = sql_main.process_query(query)
assert result is False
mock_logger.error.assert_called_once()
assert 'Problem connecting to db' in str(mock_logger.error.call_args)
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_process_query_returns_empty_list(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test query returns empty list when no results"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.execute_query = MagicMock(return_value=[])
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
query = "SELECT * FROM test WHERE 1=0"
result = sql_main.process_query(query)
assert result == []
# Integration-like tests
class TestSQLMainIntegration:
"""Integration-like tests for complete workflows"""
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_full_workflow_connect_query_close(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test complete workflow: connect, query, close"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.execute_query = MagicMock(return_value=[{'count': 5}])
mock_sqlite_instance.db_close = MagicMock()
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
# Execute query
result = sql_main.process_query("SELECT COUNT(*) as count FROM test")
assert result == [{'count': 5}]
# Check connected
assert sql_main.connected() is True
# Close connection
sql_main.close()
mock_sqlite_instance.db_close.assert_called_once()
@patch('corelibs.db_handling.sql_main.SQLiteIO')
def test_multiple_queries_same_connection(
self, mock_sqlite_class: MagicMock, mock_logger: MagicMock, temp_db_path: Path
):
"""Test multiple queries on the same connection"""
mock_sqlite_instance = MagicMock()
mock_sqlite_instance.conn = MagicMock()
mock_sqlite_instance.db_connected = MagicMock(return_value=True)
mock_sqlite_instance.execute_query = MagicMock(side_effect=[
[{'id': 1}],
[{'id': 2}],
[{'id': 3}]
])
mock_sqlite_class.return_value = mock_sqlite_instance
db_ident = f'sqlite:{temp_db_path}'
sql_main = SQLMain(mock_logger, db_ident)
result1 = sql_main.process_query("SELECT * FROM test WHERE id = 1")
result2 = sql_main.process_query("SELECT * FROM test WHERE id = 2")
result3 = sql_main.process_query("SELECT * FROM test WHERE id = 3")
assert result1 == [{'id': 1}]
assert result2 == [{'id': 2}]
assert result3 == [{'id': 3}]
assert mock_sqlite_instance.execute_query.call_count == 3
# __END__

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,538 @@
"""
PyTest: file_handling/file_bom_encoding
"""
from pathlib import Path
import pytest
from corelibs.file_handling.file_bom_encoding import (
is_bom_encoded,
is_bom_encoded_info,
BomEncodingInfo,
)
class TestIsBomEncoded:
"""Test suite for is_bom_encoded function"""
def test_utf8_bom_file(self, tmp_path: Path):
"""Test detection of UTF-8 BOM encoded file"""
test_file = tmp_path / "utf8_bom.txt"
# UTF-8 BOM: EF BB BF
content = b'\xef\xbb\xbfHello, World!'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
assert isinstance(result, bool)
def test_utf16_le_bom_file(self, tmp_path: Path):
"""Test detection of UTF-16 LE BOM encoded file"""
test_file = tmp_path / "utf16_le_bom.txt"
# UTF-16 LE BOM: FF FE
content = b'\xff\xfeH\x00e\x00l\x00l\x00o\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf16_be_bom_file(self, tmp_path: Path):
"""Test detection of UTF-16 BE BOM encoded file"""
test_file = tmp_path / "utf16_be_bom.txt"
# UTF-16 BE BOM: FE FF
content = b'\xfe\xff\x00H\x00e\x00l\x00l\x00o'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf32_le_bom_file(self, tmp_path: Path):
"""Test detection of UTF-32 LE BOM encoded file"""
test_file = tmp_path / "utf32_le_bom.txt"
# UTF-32 LE BOM: FF FE 00 00
content = b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_utf32_be_bom_file(self, tmp_path: Path):
"""Test detection of UTF-32 BE BOM encoded file"""
test_file = tmp_path / "utf32_be_bom.txt"
# UTF-32 BE BOM: 00 00 FE FF
content = b'\x00\x00\xfe\xff\x00\x00\x00H\x00\x00\x00e'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_no_bom_ascii_file(self, tmp_path: Path):
"""Test detection of ASCII file without BOM"""
test_file = tmp_path / "ascii.txt"
content = b'Hello, World!'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_no_bom_utf8_file(self, tmp_path: Path):
"""Test detection of UTF-8 file without BOM"""
test_file = tmp_path / "utf8_no_bom.txt"
content = 'Hello, 世界!'.encode('utf-8')
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_empty_file(self, tmp_path: Path):
"""Test detection on empty file"""
test_file = tmp_path / "empty.txt"
test_file.write_bytes(b'')
result = is_bom_encoded(test_file)
assert result is False
def test_binary_file_no_bom(self, tmp_path: Path):
"""Test detection on binary file without BOM"""
test_file = tmp_path / "binary.bin"
content = bytes(range(256))
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_partial_bom_pattern(self, tmp_path: Path):
"""Test file with partial BOM pattern that shouldn't match"""
test_file = tmp_path / "partial_bom.txt"
# Only first two bytes of UTF-8 BOM
content = b'\xef\xbbHello'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_false_positive_bom_pattern(self, tmp_path: Path):
"""Test file that contains BOM-like bytes but not at the start"""
test_file = tmp_path / "false_positive.txt"
content = b'Hello\xef\xbb\xbfWorld'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_nonexistent_file(self, tmp_path: Path):
"""Test that function raises error for non-existent file"""
test_file = tmp_path / "nonexistent.txt"
with pytest.raises(ValueError, match="Error checking BOM encoding"):
is_bom_encoded(test_file)
def test_very_small_file(self, tmp_path: Path):
"""Test file smaller than largest BOM pattern (4 bytes)"""
test_file = tmp_path / "small.txt"
content = b'Hi'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is False
def test_exactly_bom_size_utf8(self, tmp_path: Path):
"""Test file that is exactly the size of UTF-8 BOM"""
test_file = tmp_path / "exact_bom.txt"
content = b'\xef\xbb\xbf'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
def test_exactly_bom_size_utf32(self, tmp_path: Path):
"""Test file that is exactly the size of UTF-32 BOM"""
test_file = tmp_path / "exact_bom_utf32.txt"
content = b'\xff\xfe\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded(test_file)
assert result is True
class TestIsBomEncodedInfo:
"""Test suite for is_bom_encoded_info function"""
def test_utf8_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-8 BOM encoded file"""
test_file = tmp_path / "utf8_bom.txt"
content = b'\xef\xbb\xbfHello, UTF-8!'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert isinstance(result, dict)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
assert result['bom_length'] == 3
assert result['bom_pattern'] == b'\xef\xbb\xbf'
def test_utf16_le_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-16 LE BOM encoded file"""
test_file = tmp_path / "utf16_le_bom.txt"
content = b'\xff\xfeH\x00e\x00l\x00l\x00o\x00'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-16 LE'
assert result['encoding'] == 'utf-16-le'
assert result['bom_length'] == 2
assert result['bom_pattern'] == b'\xff\xfe'
def test_utf16_be_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-16 BE BOM encoded file"""
test_file = tmp_path / "utf16_be_bom.txt"
content = b'\xfe\xff\x00H\x00e\x00l\x00l\x00o'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-16 BE'
assert result['encoding'] == 'utf-16-be'
assert result['bom_length'] == 2
assert result['bom_pattern'] == b'\xfe\xff'
def test_utf32_le_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-32 LE BOM encoded file"""
test_file = tmp_path / "utf32_le_bom.txt"
content = b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 LE'
assert result['encoding'] == 'utf-32-le'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\xff\xfe\x00\x00'
def test_utf32_be_bom_info(self, tmp_path: Path):
"""Test detailed info for UTF-32 BE BOM encoded file"""
test_file = tmp_path / "utf32_be_bom.txt"
content = b'\x00\x00\xfe\xff\x00\x00\x00H\x00\x00\x00e'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 BE'
assert result['encoding'] == 'utf-32-be'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\x00\x00\xfe\xff'
def test_no_bom_info(self, tmp_path: Path):
"""Test detailed info for file without BOM"""
test_file = tmp_path / "no_bom.txt"
content = b'Hello, World!'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
assert result['bom_pattern'] is None
def test_empty_file_info(self, tmp_path: Path):
"""Test detailed info for empty file"""
test_file = tmp_path / "empty.txt"
test_file.write_bytes(b'')
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
assert result['bom_pattern'] is None
def test_bom_precedence_utf32_vs_utf16(self, tmp_path: Path):
"""Test that UTF-32 LE BOM takes precedence over UTF-16 LE when both match"""
test_file = tmp_path / "precedence.txt"
# UTF-32 LE BOM starts with UTF-16 LE BOM pattern
content = b'\xff\xfe\x00\x00Additional content'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
# Should detect UTF-32 LE, not UTF-16 LE
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-32 LE'
assert result['encoding'] == 'utf-32-le'
assert result['bom_length'] == 4
assert result['bom_pattern'] == b'\xff\xfe\x00\x00'
def test_return_type_validation(self, tmp_path: Path):
"""Test that return type matches BomEncodingInfo TypedDict"""
test_file = tmp_path / "test.txt"
test_file.write_bytes(b'Test content')
result = is_bom_encoded_info(test_file)
# Check all required keys are present
required_keys = {'has_bom', 'bom_type', 'encoding', 'bom_length', 'bom_pattern'}
assert set(result.keys()) == required_keys
# Check types
assert isinstance(result['has_bom'], bool)
assert result['bom_type'] is None or isinstance(result['bom_type'], str)
assert result['encoding'] is None or isinstance(result['encoding'], str)
assert isinstance(result['bom_length'], int)
assert result['bom_pattern'] is None or isinstance(result['bom_pattern'], bytes)
def test_nonexistent_file_error(self, tmp_path: Path):
"""Test that function raises ValueError for non-existent file"""
test_file = tmp_path / "nonexistent.txt"
with pytest.raises(ValueError) as exc_info:
is_bom_encoded_info(test_file)
assert "Error checking BOM encoding" in str(exc_info.value)
def test_directory_instead_of_file(self, tmp_path: Path):
"""Test that function raises error when given a directory"""
test_dir = tmp_path / "test_dir"
test_dir.mkdir()
with pytest.raises(ValueError, match="Error checking BOM encoding"):
is_bom_encoded_info(test_dir)
def test_large_file_with_bom(self, tmp_path: Path):
"""Test BOM detection on large file (only first 4 bytes matter)"""
test_file = tmp_path / "large_bom.txt"
# UTF-8 BOM followed by large content
content = b'\xef\xbb\xbf' + b'A' * 100000
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
def test_bom_detection_priority_order(self, tmp_path: Path):
"""Test that BOM patterns are checked in the correct priority order"""
# The function should check longer patterns first to avoid false matches
test_cases = [
(b'\xff\xfe\x00\x00', 'UTF-32 LE'), # 4 bytes
(b'\x00\x00\xfe\xff', 'UTF-32 BE'), # 4 bytes
(b'\xff\xfe', 'UTF-16 LE'), # 2 bytes
(b'\xfe\xff', 'UTF-16 BE'), # 2 bytes
(b'\xef\xbb\xbf', 'UTF-8'), # 3 bytes
]
for i, (bom_bytes, expected_type) in enumerate(test_cases):
test_file = tmp_path / f"priority_test_{i}.txt"
content = bom_bytes + b'Content'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['bom_type'] == expected_type
assert result['bom_pattern'] == bom_bytes
def test_csv_file_with_utf8_bom(self, tmp_path: Path):
"""Test CSV file with UTF-8 BOM (common use case mentioned in docstring)"""
test_file = tmp_path / "data.csv"
content = b'\xef\xbb\xbf"Name","Age","City"\n"John",30,"New York"\n"Jane",25,"Tokyo"'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is True
assert result['bom_type'] == 'UTF-8'
assert result['encoding'] == 'utf-8'
assert result['bom_length'] == 3
def test_csv_file_without_bom(self, tmp_path: Path):
"""Test CSV file without BOM"""
test_file = tmp_path / "data_no_bom.csv"
content = b'"Name","Age","City"\n"John",30,"New York"\n"Jane",25,"Tokyo"'
test_file.write_bytes(content)
result = is_bom_encoded_info(test_file)
assert result['has_bom'] is False
assert result['bom_type'] is None
assert result['encoding'] is None
assert result['bom_length'] == 0
class TestBomEncodingInfo:
"""Test suite for BomEncodingInfo TypedDict"""
def test_typed_dict_structure(self):
"""Test that BomEncodingInfo has correct structure"""
# This is a type check - in actual usage, mypy would validate this
sample_info: BomEncodingInfo = {
'has_bom': True,
'bom_type': 'UTF-8',
'encoding': 'utf-8',
'bom_length': 3,
'bom_pattern': b'\xef\xbb\xbf'
}
assert sample_info['has_bom'] is True
assert sample_info['bom_type'] == 'UTF-8'
assert sample_info['encoding'] == 'utf-8'
assert sample_info['bom_length'] == 3
assert sample_info['bom_pattern'] == b'\xef\xbb\xbf'
def test_typed_dict_none_values(self):
"""Test TypedDict with None values"""
sample_info: BomEncodingInfo = {
'has_bom': False,
'bom_type': None,
'encoding': None,
'bom_length': 0,
'bom_pattern': None
}
assert sample_info['has_bom'] is False
assert sample_info['bom_type'] is None
assert sample_info['encoding'] is None
assert sample_info['bom_length'] == 0
assert sample_info['bom_pattern'] is None
class TestIntegration:
"""Integration tests for BOM encoding detection"""
def test_is_bom_encoded_uses_info_function(self, tmp_path: Path):
"""Test that is_bom_encoded uses is_bom_encoded_info internally"""
test_file = tmp_path / "integration.txt"
content = b'\xef\xbb\xbfIntegration test'
test_file.write_bytes(content)
# Both functions should return consistent results
simple_result = is_bom_encoded(test_file)
detailed_result = is_bom_encoded_info(test_file)
assert simple_result == detailed_result['has_bom']
assert simple_result is True
def test_multiple_file_bom_detection_workflow(self, tmp_path: Path):
"""Test a workflow of detecting BOM across multiple files"""
files = {
'utf8_bom.csv': b'\xef\xbb\xbf"data","value"\n"test",123',
'utf16_le.txt': b'\xff\xfeH\x00e\x00l\x00l\x00o\x00',
'no_bom.txt': b'Plain ASCII text',
'empty.txt': b'',
}
results = {}
detailed_results = {}
for filename, content in files.items():
file_path = tmp_path / filename
file_path.write_bytes(content)
results[filename] = is_bom_encoded(file_path)
detailed_results[filename] = is_bom_encoded_info(file_path)
# Verify results
assert results['utf8_bom.csv'] is True
assert results['utf16_le.txt'] is True
assert results['no_bom.txt'] is False
assert results['empty.txt'] is False
# Verify detailed results match simple results
for filename in files:
assert results[filename] == detailed_results[filename]['has_bom']
# Verify specific encoding details
assert detailed_results['utf8_bom.csv']['encoding'] == 'utf-8'
assert detailed_results['utf16_le.txt']['encoding'] == 'utf-16-le'
assert detailed_results['no_bom.txt']['encoding'] is None
def test_csv_loading_workflow(self, tmp_path: Path):
"""Test BOM detection workflow for CSV loading (main use case)"""
# Create CSV files with and without BOM
csv_with_bom = tmp_path / "data_with_bom.csv"
csv_without_bom = tmp_path / "data_without_bom.csv"
# CSV with UTF-8 BOM
bom_content = b'\xef\xbb\xbf"Name","Age"\n"Alice",30\n"Bob",25'
csv_with_bom.write_bytes(bom_content)
# CSV without BOM
no_bom_content = b'"Name","Age"\n"Charlie",35\n"Diana",28'
csv_without_bom.write_bytes(no_bom_content)
# Simulate CSV loading workflow
files_to_process = [csv_with_bom, csv_without_bom]
processing_info: list[dict[str, str | bool | int]] = []
for csv_file in files_to_process:
bom_info = is_bom_encoded_info(csv_file)
file_info: dict[str, str | bool | int] = {
'file': csv_file.name,
'has_bom': bom_info['has_bom'],
'encoding': bom_info['encoding'] or 'default',
'skip_bytes': bom_info['bom_length']
}
processing_info.append(file_info)
# Verify workflow results
assert len(processing_info) == 2
bom_file_info = next(info for info in processing_info if info['file'] == 'data_with_bom.csv')
no_bom_file_info = next(info for info in processing_info if info['file'] == 'data_without_bom.csv')
assert bom_file_info['has_bom'] is True
assert bom_file_info['encoding'] == 'utf-8'
assert bom_file_info['skip_bytes'] == 3
assert no_bom_file_info['has_bom'] is False
assert no_bom_file_info['encoding'] == 'default'
assert no_bom_file_info['skip_bytes'] == 0
def test_error_handling_consistency(self, tmp_path: Path):
"""Test that both functions handle errors consistently"""
nonexistent_file = tmp_path / "does_not_exist.txt"
# Both functions should raise ValueError for non-existent files
with pytest.raises(ValueError):
is_bom_encoded(nonexistent_file)
with pytest.raises(ValueError):
is_bom_encoded_info(nonexistent_file)
def test_all_supported_bom_types(self, tmp_path: Path):
"""Test detection of all supported BOM types"""
bom_test_cases = [
('utf8', b'\xef\xbb\xbf', 'UTF-8', 'utf-8', 3),
('utf16_le', b'\xff\xfe', 'UTF-16 LE', 'utf-16-le', 2),
('utf16_be', b'\xfe\xff', 'UTF-16 BE', 'utf-16-be', 2),
('utf32_le', b'\xff\xfe\x00\x00', 'UTF-32 LE', 'utf-32-le', 4),
('utf32_be', b'\x00\x00\xfe\xff', 'UTF-32 BE', 'utf-32-be', 4),
]
for name, bom_bytes, expected_type, expected_encoding, expected_length in bom_test_cases:
test_file = tmp_path / f"{name}_test.txt"
content = bom_bytes + b'Test content'
test_file.write_bytes(content)
# Test simple function
assert is_bom_encoded(test_file) is True
# Test detailed function
info = is_bom_encoded_info(test_file)
assert info['has_bom'] is True
assert info['bom_type'] == expected_type
assert info['encoding'] == expected_encoding
assert info['bom_length'] == expected_length
assert info['bom_pattern'] == bom_bytes
# __END__

View File

@@ -4,7 +4,101 @@ tests for corelibs.iterator_handling.fingerprint
from typing import Any
import pytest
from corelibs.iterator_handling.fingerprint import dict_hash_frozen, dict_hash_crc
from corelibs.iterator_handling.fingerprint import dict_hash_frozen, dict_hash_crc, hash_object
class TestHashObject:
"""Tests for hash_object function"""
def test_hash_object_simple_dict(self):
"""Test hashing a simple dictionary with hash_object"""
data = {"key1": "value1", "key2": "value2"}
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64 # SHA256 produces 64 hex characters
def test_hash_object_mixed_keys(self):
"""Test hash_object with mixed int and string keys"""
data = {"key1": "value1", 1: "value2", 2: "value3"}
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64
def test_hash_object_consistency(self):
"""Test that hash_object produces consistent results"""
data = {"str_key": "value", 123: "number_key"}
hash1 = hash_object(data)
hash2 = hash_object(data)
assert hash1 == hash2
def test_hash_object_order_independence(self):
"""Test that hash_object is order-independent"""
data1 = {"a": 1, 1: "one", "b": 2, 2: "two"}
data2 = {2: "two", "b": 2, 1: "one", "a": 1}
hash1 = hash_object(data1)
hash2 = hash_object(data2)
assert hash1 == hash2
def test_hash_object_list_of_dicts_mixed_keys(self):
"""Test hash_object with list of dicts containing mixed keys"""
data = [
{"name": "item1", 1: "value1"},
{"name": "item2", 2: "value2"}
]
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64
def test_hash_object_nested_mixed_keys(self):
"""Test hash_object with nested structures containing mixed keys"""
data = {
"outer": {
"inner": "value",
1: "mixed_key"
},
2: "another_mixed"
}
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64
def test_hash_object_different_data(self):
"""Test that different data produces different hashes"""
data1 = {"key": "value", 1: "one"}
data2 = {"key": "value", 2: "two"}
hash1 = hash_object(data1)
hash2 = hash_object(data2)
assert hash1 != hash2
def test_hash_object_complex_nested(self):
"""Test hash_object with complex nested structures"""
data = {
"level1": {
"level2": {
1: "value",
"key": [1, 2, {"nested": "deep", 3: "int_key"}]
}
}
}
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64
def test_hash_object_list_with_tuples(self):
"""Test hash_object with lists containing tuples"""
data = [("a", 1), ("b", 2), {1: "mixed", "key": "value"}]
result = hash_object(data)
assert isinstance(result, str)
assert len(result) == 64
class TestDictHashFrozen:
@@ -279,6 +373,116 @@ class TestDictHashCrc:
assert isinstance(result, str)
assert len(result) == 64
def test_dict_hash_crc_fallback_mixed_keys(self):
"""Test dict_hash_crc fallback with mixed int and string keys"""
data = {"key1": "value1", 1: "value2", 2: "value3"}
result = dict_hash_crc(data)
assert isinstance(result, str)
# Fallback prefixes with "HO_"
assert result.startswith("HO_")
# Hash should be 64 chars + 3 char prefix = 67 total
assert len(result) == 67
def test_dict_hash_crc_fallback_consistency(self):
"""Test that fallback produces consistent hashes"""
data = {"str_key": "value", 123: "number_key", 456: "another"}
hash1 = dict_hash_crc(data)
hash2 = dict_hash_crc(data)
assert hash1 == hash2
assert hash1.startswith("HO_")
def test_dict_hash_crc_fallback_order_independence(self):
"""Test that fallback is order-independent for mixed-key dicts"""
data1 = {"a": 1, 1: "one", "b": 2, 2: "two"}
data2 = {2: "two", "b": 2, 1: "one", "a": 1}
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 == hash2
assert hash1.startswith("HO_")
def test_dict_hash_crc_fallback_list_of_dicts_mixed_keys(self):
"""Test fallback with list of dicts containing mixed keys"""
data = [
{"name": "item1", 1: "value1"},
{"name": "item2", 2: "value2"},
{3: "value3", "type": "mixed"}
]
result = dict_hash_crc(data)
assert isinstance(result, str)
assert result.startswith("HO_")
assert len(result) == 67
def test_dict_hash_crc_fallback_nested_mixed_keys(self):
"""Test fallback with nested dicts containing mixed keys"""
data = {
"outer": {
"inner": "value",
1: "mixed_key"
},
2: "another_mixed"
}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert result.startswith("HO_")
assert len(result) == 67
def test_dict_hash_crc_fallback_different_data(self):
"""Test that different mixed-key data produces different hashes"""
data1 = {"key": "value", 1: "one"}
data2 = {"key": "value", 2: "two"}
hash1 = dict_hash_crc(data1)
hash2 = dict_hash_crc(data2)
assert hash1 != hash2
assert hash1.startswith("HO_")
assert hash2.startswith("HO_")
def test_dict_hash_crc_fallback_complex_structure(self):
"""Test fallback with complex nested structure with mixed keys"""
data = [
{
"id": 1,
1: "first",
"data": {
"nested": "value",
100: "nested_int_key"
}
},
{
"id": 2,
2: "second",
"items": [1, 2, 3]
}
]
result = dict_hash_crc(data)
assert isinstance(result, str)
assert result.startswith("HO_")
assert len(result) == 67
def test_dict_hash_crc_no_fallback_string_keys_only(self):
"""Test that string-only keys don't trigger fallback"""
data = {"key1": "value1", "key2": "value2", "key3": "value3"}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert not result.startswith("HO_")
assert len(result) == 64
def test_dict_hash_crc_no_fallback_int_keys_only(self):
"""Test that int-only keys don't trigger fallback"""
data = {1: "one", 2: "two", 3: "three"}
result = dict_hash_crc(data)
assert isinstance(result, str)
assert not result.startswith("HO_")
assert len(result) == 64
class TestComparisonBetweenHashFunctions:
"""Tests comparing dict_hash_frozen and dict_hash_crc"""

View File

@@ -4,7 +4,7 @@ iterator_handling.list_helepr tests
from typing import Any
import pytest
from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list
from corelibs.iterator_handling.list_helpers import convert_to_list, is_list_in_list, make_unique_list_of_dicts
class TestConvertToList:
@@ -298,3 +298,225 @@ class TestPerformance:
# Should still work correctly despite duplicates
assert set(result) == {1, 3}
assert isinstance(result, list)
class TestMakeUniqueListOfDicts:
"""Test cases for make_unique_list_of_dicts function"""
def test_basic_duplicate_removal(self):
"""Test basic removal of duplicate dictionaries"""
dict_list = [
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"a": 3, "b": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": 1, "b": 2} in result
assert {"a": 3, "b": 4} in result
def test_order_independent_duplicates(self):
"""Test that dictionaries with different key orders are treated as duplicates"""
dict_list = [
{"a": 1, "b": 2},
{"b": 2, "a": 1}, # Same content, different order
{"a": 3, "b": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": 1, "b": 2} in result
assert {"a": 3, "b": 4} in result
def test_empty_list(self):
"""Test with empty list"""
result = make_unique_list_of_dicts([])
assert result == []
assert isinstance(result, list)
def test_single_dict(self):
"""Test with single dictionary"""
dict_list = [{"a": 1, "b": 2}]
result = make_unique_list_of_dicts(dict_list)
assert result == [{"a": 1, "b": 2}]
def test_all_unique(self):
"""Test when all dictionaries are unique"""
dict_list = [
{"a": 1},
{"b": 2},
{"c": 3},
{"d": 4}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 4
for d in dict_list:
assert d in result
def test_all_duplicates(self):
"""Test when all dictionaries are duplicates"""
dict_list = [
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"a": 1, "b": 2},
{"b": 2, "a": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 1
assert result[0] == {"a": 1, "b": 2}
def test_nested_values(self):
"""Test with nested structures as values"""
dict_list = [
{"a": [1, 2], "b": 3},
{"a": [1, 2], "b": 3},
{"a": [1, 3], "b": 3}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": [1, 2], "b": 3} in result
assert {"a": [1, 3], "b": 3} in result
def test_different_value_types(self):
"""Test with different value types"""
dict_list = [
{"str": "hello", "int": 42, "float": 3.14, "bool": True},
{"str": "hello", "int": 42, "float": 3.14, "bool": True},
{"str": "world", "int": 99, "float": 2.71, "bool": False}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_empty_dicts(self):
"""Test with empty dictionaries"""
dict_list: list[Any] = [
{},
{},
{"a": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {} in result
assert {"a": 1} in result
def test_single_key_dicts(self):
"""Test with single key dictionaries"""
dict_list = [
{"a": 1},
{"a": 1},
{"a": 2},
{"b": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 3
assert {"a": 1} in result
assert {"a": 2} in result
assert {"b": 1} in result
def test_many_keys(self):
"""Test with dictionaries containing many keys"""
dict1 = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}
dict2 = {"e": 5, "d": 4, "c": 3, "b": 2, "a": 1} # Same, different order
dict3 = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 6} # Different value
dict_list = [dict1, dict2, dict3]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_numeric_keys(self):
"""Test with numeric keys"""
dict_list = [
{1: "one", 2: "two"},
{2: "two", 1: "one"},
{1: "one", 2: "three"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_none_values(self):
"""Test with None values"""
dict_list = [
{"a": None, "b": 2},
{"a": None, "b": 2},
{"a": 1, "b": None}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"a": None, "b": 2} in result
assert {"a": 1, "b": None} in result
def test_mixed_key_types(self):
"""Test with mixed key types (string and numeric)"""
dict_list = [
{"a": 1, 1: "one"},
{1: "one", "a": 1},
{"a": 2, 1: "one"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
@pytest.mark.parametrize("dict_list,expected_length", [
([{"a": 1}, {"a": 1}, {"a": 1}], 1),
([{"a": 1}, {"a": 2}, {"a": 3}], 3),
([{"a": 1, "b": 2}, {"b": 2, "a": 1}], 1),
([{}, {}], 1),
([{"x": [1, 2]}, {"x": [1, 2]}], 1),
([{"a": 1}, {"b": 2}, {"c": 3}], 3),
]) # pyright: ignore[reportUnknownArgumentType]
def test_parametrized_unique_dicts(self, dict_list: list[Any], expected_length: int):
"""Test make_unique_list_of_dicts with various input combinations"""
result = make_unique_list_of_dicts(dict_list)
assert len(result) == expected_length
assert isinstance(result, list)
def test_large_list(self):
"""Test with a large list of dictionaries"""
dict_list = [{"id": i % 100, "value": f"val_{i % 100}"} for i in range(1000)]
result = make_unique_list_of_dicts(dict_list)
# Should have 100 unique dicts (0-99)
assert len(result) == 100
def test_preserves_last_occurrence(self):
"""Test behavior with duplicate entries"""
# The function uses dict comprehension, which keeps last occurrence
dict_list = [
{"a": 1, "b": 2},
{"a": 3, "b": 4},
{"a": 1, "b": 2}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
# Just verify correct unique count, order may vary
def test_nested_dicts(self):
"""Test with nested dictionaries"""
dict_list = [
{"outer": {"inner": 1}},
{"outer": {"inner": 1}},
{"outer": {"inner": 2}}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
def test_string_values_case_sensitive(self):
"""Test that string values are case-sensitive"""
dict_list = [
{"name": "John"},
{"name": "john"},
{"name": "JOHN"},
{"name": "John"}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 3
def test_boolean_values(self):
"""Test with boolean values"""
dict_list = [
{"flag": True, "count": 1},
{"count": 1, "flag": True},
{"flag": False, "count": 1}
]
result = make_unique_list_of_dicts(dict_list)
assert len(result) == 2
assert {"flag": True, "count": 1} in result
assert {"flag": False, "count": 1} in result
# __END__

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +34,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -99,6 +101,85 @@ class TestLogSettingsParsing:
assert log.log_settings["console_enabled"] == Log.DEFAULT_LOG_SETTINGS["console_enabled"]
assert log.log_settings["per_run_log"] == Log.DEFAULT_LOG_SETTINGS["per_run_log"]
def test_parse_console_format_type_all(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to ALL"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.ALL,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.ALL
def test_parse_console_format_type_condensed(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to CONDENSED"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.CONDENSED,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.CONDENSED
def test_parse_console_format_type_minimal(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to MINIMAL"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.MINIMAL,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.MINIMAL
def test_parse_console_format_type_bare(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to BARE"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.BARE,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.BARE
def test_parse_console_format_type_none(self, tmp_log_path: Path):
"""Test parsing with console_format_type set to NONE"""
settings: dict[str, Any] = {
"console_format_type": ConsoleFormatSettings.NONE,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["console_format_type"] == ConsoleFormatSettings.NONE
def test_parse_console_format_type_invalid(self, tmp_log_path: Path):
"""Test parsing with invalid console_format_type raises TypeError"""
settings: dict[str, Any] = {
"console_format_type": "invalid_format",
}
# Invalid console_format_type causes TypeError during handler creation
# because the code doesn't validate the type before using it
with pytest.raises(TypeError, match="'in <string>' requires string as left operand"):
Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
# MARK: Test Spacer Constants
class TestSpacerConstants:
@@ -115,6 +196,70 @@ class TestSpacerConstants:
assert LogParent.SPACER_LENGTH == 32
# MARK: Test ConsoleFormatSettings.from_string
class TestConsoleFormatSettingsFromString:
"""Test cases for ConsoleFormatSettings.from_string method"""
def test_from_string_all(self):
"""Test from_string with 'ALL' returns correct format"""
result = ConsoleFormatSettings.from_string('ALL')
assert result == ConsoleFormatSettings.ALL
def test_from_string_condensed(self):
"""Test from_string with 'CONDENSED' returns correct format"""
result = ConsoleFormatSettings.from_string('CONDENSED')
assert result == ConsoleFormatSettings.CONDENSED
def test_from_string_minimal(self):
"""Test from_string with 'MINIMAL' returns correct format"""
result = ConsoleFormatSettings.from_string('MINIMAL')
assert result == ConsoleFormatSettings.MINIMAL
def test_from_string_bare(self):
"""Test from_string with 'BARE' returns correct format"""
result = ConsoleFormatSettings.from_string('BARE')
assert result == ConsoleFormatSettings.BARE
def test_from_string_none(self):
"""Test from_string with 'NONE' returns correct format"""
result = ConsoleFormatSettings.from_string('NONE')
assert result == ConsoleFormatSettings.NONE
def test_from_string_invalid_returns_none(self):
"""Test from_string with invalid string returns None"""
result = ConsoleFormatSettings.from_string('INVALID')
assert result is None
def test_from_string_invalid_with_default(self):
"""Test from_string with invalid string returns provided default"""
default = ConsoleFormatSettings.ALL
result = ConsoleFormatSettings.from_string('INVALID', default=default)
assert result == default
def test_from_string_case_sensitive(self):
"""Test from_string is case sensitive"""
# Lowercase should not match
result = ConsoleFormatSettings.from_string('all')
assert result is None
def test_from_string_with_none_default(self):
"""Test from_string with explicit None default"""
result = ConsoleFormatSettings.from_string('NONEXISTENT', default=None)
assert result is None
@pytest.mark.parametrize("setting_name,expected", [
("ALL", ConsoleFormatSettings.ALL),
("CONDENSED", ConsoleFormatSettings.CONDENSED),
("MINIMAL", ConsoleFormatSettings.MINIMAL),
("BARE", ConsoleFormatSettings.BARE),
("NONE", ConsoleFormatSettings.NONE),
])
def test_from_string_all_valid_settings(self, setting_name: str, expected: Any):
"""Test from_string with all valid setting names"""
result = ConsoleFormatSettings.from_string(setting_name)
assert result == expected
# MARK: Parametrized Tests
class TestParametrized:
"""Parametrized tests for comprehensive coverage"""
@@ -160,6 +305,7 @@ class TestParametrized:
("per_run_log", True, "not_bool"),
("console_enabled", False, 123),
("console_color_output_enabled", True, None),
("console_format_type", ConsoleFormatSettings.ALL, "invalid_format"),
("add_start_info", False, []),
("add_end_info", True, {}),
])

View File

@@ -13,6 +13,7 @@ from corelibs.logging_handling.log import (
LogParent,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -35,6 +36,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -169,6 +171,7 @@ class TestLogInitialization:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -190,6 +193,7 @@ class TestLogInitialization:
"per_run_log": True,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -256,6 +260,7 @@ class TestLogInitialization:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -433,4 +438,81 @@ class TestLogLevelHandling:
level = log_instance.get_log_level("file_handler")
assert level == LoggingLevel.DEBUG
class DummyHandler:
"""Dummy log level handler"""
def __init__(self, level: LoggingLevel):
self.level = level
@pytest.fixture
def log_instance_level() -> Log:
"""
Minimal log instance with dummy handlers
Returns:
Log -- _description_
"""
log = Log(
log_path=Path("/tmp/test.log"),
log_name="test",
log_settings={
"log_level_console": LoggingLevel.DEBUG,
"log_level_file": LoggingLevel.DEBUG,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": None,
"per_run_log": False,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
}
)
return log
def test_any_handler_is_minimum_level_true(log_instance_level: Log):
"""Test any_handler_is_minimum_level returns True when a handler meets the level"""
# Handler with DEBUG level, should include INFO
log_instance_level.handlers = {
"h1": DummyHandler(LoggingLevel.DEBUG)
}
assert log_instance_level.any_handler_is_minimum_level(LoggingLevel.INFO) is True
def test_any_handler_is_minimum_level_false(log_instance_level: Log):
"""Test any_handler_is_minimum_level returns False when no handler meets the level"""
# Handler with WARNING level, should include ERROR
log_instance_level.handlers = {
"h1": DummyHandler(LoggingLevel.WARNING)
}
assert log_instance_level.any_handler_is_minimum_level(LoggingLevel.ERROR) is True
def test_any_handler_is_minimum_level_multiple(log_instance_level: Log):
"""Test any_handler_is_minimum_level with multiple handlers"""
# Multiple handlers, one matches
log_instance_level.handlers = {
"h1": DummyHandler(LoggingLevel.ERROR),
"h2": DummyHandler(LoggingLevel.DEBUG)
}
assert log_instance_level.any_handler_is_minimum_level(LoggingLevel.INFO) is True
# None matches
log_instance_level.handlers = {
"h1": DummyHandler(LoggingLevel.ERROR),
"h2": DummyHandler(LoggingLevel.CRITICAL)
}
assert log_instance_level.any_handler_is_minimum_level(LoggingLevel.DEBUG) is False
def test_any_handler_is_minimum_level_handles_exceptions(log_instance_level: Log):
"""Test any_handler_is_minimum_level handles exceptions gracefully"""
# Handler with missing level attribute
class BadHandler:
pass
log_instance_level.handlers = {
"h1": BadHandler()
}
# Should not raise, just return False
assert log_instance_level.any_handler_is_minimum_level(LoggingLevel.DEBUG) is False
# __END__

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -27,12 +28,14 @@ def tmp_log_path(tmp_path: Path) -> Path:
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
# Return a new dict each time to avoid state pollution
return {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -138,4 +141,222 @@ class TestCustomConsoleFormatter:
assert "Critical message" in result
assert "CRITICAL" in result
# MARK: Test update_console_formatter
class TestUpdateConsoleFormatter:
"""Test cases for update_console_formatter method"""
def test_update_console_formatter_to_minimal(self, log_instance: Log):
"""Test updating console formatter to MINIMAL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_condensed(self, log_instance: Log):
"""Test updating console formatter to CONDENSED format"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_bare(self, log_instance: Log):
"""Test updating console formatter to BARE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_none(self, log_instance: Log):
"""Test updating console formatter to NONE format"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_to_all(self, log_instance: Log):
"""Test updating console formatter to ALL format"""
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter was updated
assert formatter is not None
def test_update_console_formatter_when_disabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test that update_console_formatter does nothing when console is disabled"""
# Disable console
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# This should not raise an error and should return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Verify console handler doesn't exist
assert log.CONSOLE_HANDLER not in log.handlers
def test_update_console_formatter_with_color_enabled(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test updating console formatter with color output enabled"""
basic_log_settings['console_color_output_enabled'] = True
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log.handlers[log.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is CustomConsoleFormatter when colors enabled
assert isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_without_color(self, log_instance: Log):
"""Test updating console formatter without color output"""
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Get the console handler's formatter
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter = console_handler.formatter
# Verify formatter is standard Formatter when colors disabled
assert isinstance(formatter, logging.Formatter)
# But not the colored version
assert not isinstance(formatter, CustomConsoleFormatter)
def test_update_console_formatter_multiple_times(self, log_instance: Log):
"""Test updating console formatter multiple times"""
# Update to MINIMAL
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
console_handler = log_instance.handlers[log_instance.CONSOLE_HANDLER]
formatter1 = console_handler.formatter
# Update to CONDENSED
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter2 = console_handler.formatter
# Update to ALL
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter3 = console_handler.formatter
# Verify each update created a new formatter
assert formatter1 is not formatter2
assert formatter2 is not formatter3
assert formatter1 is not formatter3
def test_update_console_formatter_preserves_handler_level(self, log_instance: Log):
"""Test that updating formatter preserves the handler's log level"""
original_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_level = log_instance.handlers[log_instance.CONSOLE_HANDLER].level
assert original_level == new_level
def test_update_console_formatter_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that updated formatter actually affects log output"""
# Set to BARE format (message only)
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
def test_update_console_formatter_none_format_output(
self, log_instance: Log, caplog: pytest.LogCaptureFixture
):
"""Test that NONE formatter outputs only the message without any formatting"""
# Set to NONE format (message only, no level indicator)
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
# Configure caplog to capture at the appropriate level
with caplog.at_level(logging.WARNING):
log_instance.warning("Test warning message")
# Verify message was logged
assert "Test warning message" in caplog.text
def test_log_console_format_option_set_to_none(
self, tmp_log_path: Path
):
"""Test that when log_console_format option is set to None, it uses ConsoleFormatSettings.ALL"""
# Save the original DEFAULT_LOG_SETTINGS to restore it after test
original_default = Log.DEFAULT_LOG_SETTINGS.copy()
try:
# Reset DEFAULT_LOG_SETTINGS to ensure clean state
Log.DEFAULT_LOG_SETTINGS = {
"log_level_console": Log.DEFAULT_LOG_LEVEL_CONSOLE,
"log_level_file": Log.DEFAULT_LOG_LEVEL_FILE,
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": True,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": True,
"add_end_info": False,
"log_queue": None,
}
# Create a fresh settings dict with console_format_type explicitly set to None
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": None, # type: ignore
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
}
# Verify that None is explicitly set in the input
assert settings['console_format_type'] is None
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings
)
# Verify that None was replaced with ConsoleFormatSettings.ALL
# The Log class should replace None with the default value (ALL)
assert log.log_settings['console_format_type'] == ConsoleFormatSettings.ALL
finally:
# Restore original DEFAULT_LOG_SETTINGS
Log.DEFAULT_LOG_SETTINGS = original_default
# __END__

View File

@@ -11,6 +11,7 @@ from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomHandlerFilter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +34,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,

View File

@@ -11,6 +11,8 @@ from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
ConsoleFormat,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -33,6 +35,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -61,6 +64,7 @@ class TestHandlerManagement:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -105,4 +109,101 @@ class TestHandlerManagement:
result2 = log.add_handler("test", handler2)
assert result2 is False
def test_change_console_format_to_minimal(self, log_instance: Log):
"""Test changing console handler format to MINIMAL"""
original_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
new_formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert new_formatter is not original_formatter
assert new_formatter is not None
def test_change_console_format_to_condensed(self, log_instance: Log):
"""Test changing console handler format to CONDENSED"""
log_instance.update_console_formatter(ConsoleFormatSettings.CONDENSED)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_bare(self, log_instance: Log):
"""Test changing console handler format to BARE"""
log_instance.update_console_formatter(ConsoleFormatSettings.BARE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_none(self, log_instance: Log):
"""Test changing console handler format to NONE"""
log_instance.update_console_formatter(ConsoleFormatSettings.NONE)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_to_all(self, log_instance: Log):
"""Test changing console handler format to ALL"""
# Start with a different format
log_instance.update_console_formatter(ConsoleFormatSettings.MINIMAL)
log_instance.update_console_formatter(ConsoleFormatSettings.ALL)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
def test_change_console_format_multiple_times(self, log_instance: Log):
"""Test changing console handler format multiple times"""
formatters: list[logging.Formatter | None] = []
for format_type in [
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
ConsoleFormatSettings.ALL,
]:
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
formatters.append(formatter)
assert formatter is not None
# Verify each formatter is unique (new instance each time)
for i, formatter in enumerate(formatters):
for j, other_formatter in enumerate(formatters):
if i != j:
assert formatter is not other_formatter
def test_change_console_format_with_disabled_console(
self, tmp_log_path: Path, basic_log_settings: LogSettings
):
"""Test changing console format when console is disabled does nothing"""
basic_log_settings['console_enabled'] = False
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# Should not raise error, just return early
log.update_console_formatter(ConsoleFormatSettings.MINIMAL)
# Console handler should not exist
assert log.CONSOLE_HANDLER not in log.handlers
@pytest.mark.parametrize("format_type", [
ConsoleFormatSettings.ALL,
ConsoleFormatSettings.CONDENSED,
ConsoleFormatSettings.MINIMAL,
ConsoleFormatSettings.BARE,
ConsoleFormatSettings.NONE,
])
def test_change_console_format_parametrized(
self, log_instance: Log, format_type: ConsoleFormat # type: ignore
):
"""Test changing console format with all format types"""
log_instance.update_console_formatter(format_type)
formatter = log_instance.handlers[log_instance.CONSOLE_HANDLER].formatter
assert formatter is not None
assert isinstance(formatter, logging.Formatter)
# __END__

View File

@@ -10,6 +10,7 @@ from corelibs.logging_handling.log import (
Log,
Logger,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -32,6 +33,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,

View File

@@ -10,6 +10,7 @@ import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -32,6 +33,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -88,6 +90,7 @@ class TestEdgeCases:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": True, # Enable end info
"log_queue": None,

View File

@@ -12,6 +12,7 @@ import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
@@ -34,6 +35,7 @@ def basic_log_settings() -> LogSettings:
"per_run_log": False,
"console_enabled": True,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": None,
@@ -68,6 +70,7 @@ class TestQueueListener:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": mock_queue, # type: ignore
@@ -103,6 +106,7 @@ class TestQueueListener:
"per_run_log": False,
"console_enabled": False,
"console_color_output_enabled": False,
"console_format_type": ConsoleFormatSettings.ALL,
"add_start_info": False,
"add_end_info": False,
"log_queue": mock_queue, # type: ignore

View File

View File

@@ -0,0 +1,121 @@
"""
Unit tests for math_helpers module
"""
from corelibs.math_handling.math_helpers import gcd, lcd
class TestGcd:
"""Test cases for the gcd (Greatest Common Divisor) function"""
def test_gcd_basic_positive_numbers(self):
"""Test GCD with basic positive numbers"""
assert gcd(12, 8) == 4
assert gcd(15, 10) == 5
assert gcd(21, 14) == 7
def test_gcd_coprime_numbers(self):
"""Test GCD with coprime numbers (GCD should be 1)"""
assert gcd(13, 7) == 1
assert gcd(17, 19) == 1
assert gcd(25, 49) == 1
def test_gcd_same_numbers(self):
"""Test GCD with same numbers"""
assert gcd(5, 5) == 5
assert gcd(100, 100) == 100
def test_gcd_with_zero(self):
"""Test GCD when one or both numbers are zero"""
assert gcd(0, 5) == 5
assert gcd(5, 0) == 5
assert gcd(0, 0) == 0
def test_gcd_with_one(self):
"""Test GCD when one number is 1"""
assert gcd(1, 5) == 1
assert gcd(100, 1) == 1
def test_gcd_large_numbers(self):
"""Test GCD with large numbers"""
assert gcd(1000000, 500000) == 500000
assert gcd(123456, 789012) == 12
def test_gcd_reversed_order(self):
"""Test GCD is commutative (order doesn't matter)"""
assert gcd(12, 8) == gcd(8, 12)
assert gcd(100, 35) == gcd(35, 100)
def test_gcd_negative_numbers(self):
"""Test GCD with negative numbers"""
assert gcd(-12, 8) == 4
assert gcd(12, -8) == 4
assert gcd(-12, -8) == 4
def test_gcd_multiples(self):
"""Test GCD when one number is a multiple of the other"""
assert gcd(10, 5) == 5
assert gcd(100, 25) == 25
assert gcd(7, 21) == 7
class TestLcd:
"""Test cases for the lcd (Least Common Denominator/Multiple) function"""
def test_lcd_basic_positive_numbers(self):
"""Test LCD with basic positive numbers"""
assert lcd(4, 6) == 12
assert lcd(3, 5) == 15
assert lcd(12, 8) == 24
def test_lcd_coprime_numbers(self):
"""Test LCD with coprime numbers (should be their product)"""
assert lcd(7, 13) == 91
assert lcd(11, 13) == 143
assert lcd(5, 7) == 35
def test_lcd_same_numbers(self):
"""Test LCD with same numbers"""
assert lcd(5, 5) == 5
assert lcd(100, 100) == 100
def test_lcd_with_one(self):
"""Test LCD when one number is 1"""
assert lcd(1, 5) == 5
assert lcd(100, 1) == 100
def test_lcd_with_zero(self):
"""Test LCD when one or both numbers are zero"""
assert lcd(0, 5) == 0
assert lcd(5, 0) == 0
assert lcd(0, 0) == 0
def test_lcd_large_numbers(self):
"""Test LCD with large numbers"""
assert lcd(100, 150) == 300
assert lcd(1000, 500) == 1000
def test_lcd_reversed_order(self):
"""Test LCD is commutative (order doesn't matter)"""
assert lcd(4, 6) == lcd(6, 4)
assert lcd(12, 18) == lcd(18, 12)
def test_lcd_negative_numbers(self):
"""Test LCD with negative numbers"""
assert lcd(-4, 6) == 12
assert lcd(4, -6) == 12
assert lcd(-4, -6) == 12
def test_lcd_multiples(self):
"""Test LCD when one number is a multiple of the other"""
assert lcd(5, 10) == 10
assert lcd(3, 9) == 9
assert lcd(25, 100) == 100
def test_lcd_gcd_relationship(self):
"""Test the mathematical relationship between LCD and GCD: lcd(a,b) * gcd(a,b) = a * b"""
test_cases = [(12, 8), (15, 10), (21, 14), (100, 35)]
for a, b in test_cases:
assert lcd(a, b) * gcd(a, b) == a * b
# __END__

View File

@@ -2,11 +2,10 @@
PyTest: requests_handling/caller
"""
from typing import Any
from unittest.mock import Mock, patch
import pytest
import requests
from corelibs.requests_handling.caller import Caller
from corelibs.requests_handling.caller import Caller, ErrorResponse, ProxyConfig
class TestCallerInit:
@@ -21,13 +20,17 @@ class TestCallerInit:
assert caller.timeout == 20
assert caller.verify is True
assert caller.proxy is None
assert caller.cafile is None
assert caller.ca_file is None
def test_init_with_all_params(self):
"""Test Caller initialization with all parameters"""
header = {"Authorization": "Bearer token", "Content-Type": "application/json"}
proxy = {"http": "http://proxy.example.com:8080", "https": "https://proxy.example.com:8080"}
caller = Caller(header=header, verify=False, timeout=30, proxy=proxy)
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header=header, timeout=30, proxy=proxy, verify=False)
assert caller.headers == header
assert caller.timeout == 30
@@ -58,7 +61,7 @@ class TestCallerInit:
ca_file_path = "/path/to/ca/cert.pem"
caller = Caller(header={}, ca_file=ca_file_path)
assert caller.cafile == ca_file_path
assert caller.ca_file == ca_file_path
class TestCallerGet:
@@ -81,7 +84,8 @@ class TestCallerGet:
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -101,7 +105,8 @@ class TestCallerGet:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -134,7 +139,11 @@ class TestCallerGet:
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
proxy = {"http": "http://proxy.example.com:8080"}
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
@@ -142,40 +151,46 @@ class TestCallerGet:
assert mock_get.call_args[1]["proxies"] == proxy
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_invalid_schema_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request with invalid URL schema returns None"""
def test_get_invalid_schema_returns_none(self, mock_get: Mock):
"""Test GET request with invalid URL schema returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.get("invalid://example.com")
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'get'" in response.message
assert response.action == "get"
assert response.url == "invalid://example.com"
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_timeout_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request timeout returns None"""
def test_get_timeout_returns_none(self, mock_get: Mock):
"""Test GET request timeout returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'get'" in response.message
assert response.action == "get"
assert response.url == "https://api.example.com/data"
@patch('corelibs.requests_handling.caller.requests.get')
def test_get_connection_error_returns_none(self, mock_get: Mock, capsys: Any):
"""Test GET request connection error returns None"""
def test_get_connection_error_returns_none(self, mock_get: Mock):
"""Test GET request connection error returns ErrorResponse"""
mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'get'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'get'" in response.message
assert response.action == "get"
assert response.url == "https://api.example.com/data"
class TestCallerPost:
@@ -200,7 +215,8 @@ class TestCallerPost:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.post')
@@ -234,40 +250,46 @@ class TestCallerPost:
assert mock_post.call_args[1]["json"] == data
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_invalid_schema_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request with invalid URL schema returns None"""
def test_post_invalid_schema_returns_none(self, mock_post: Mock):
"""Test POST request with invalid URL schema returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.post("invalid://example.com", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'post'" in response.message
assert response.action == "post"
assert response.url == "invalid://example.com"
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_timeout_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request timeout returns None"""
def test_post_timeout_returns_none(self, mock_post: Mock):
"""Test POST request timeout returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.post("https://api.example.com/data", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'post'" in response.message
assert response.action == "post"
assert response.url == "https://api.example.com/data"
@patch('corelibs.requests_handling.caller.requests.post')
def test_post_connection_error_returns_none(self, mock_post: Mock, capsys: Any):
"""Test POST request connection error returns None"""
def test_post_connection_error_returns_none(self, mock_post: Mock):
"""Test POST request connection error returns ErrorResponse"""
mock_post.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.post("https://api.example.com/data", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'post'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'post'" in response.message
assert response.action == "post"
assert response.url == "https://api.example.com/data"
class TestCallerPut:
@@ -292,7 +314,8 @@ class TestCallerPut:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.put')
@@ -311,16 +334,18 @@ class TestCallerPut:
assert mock_put.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.put')
def test_put_timeout_returns_none(self, mock_put: Mock, capsys: Any):
"""Test PUT request timeout returns None"""
def test_put_timeout_returns_none(self, mock_put: Mock):
"""Test PUT request timeout returns ErrorResponse"""
mock_put.side_effect = requests.exceptions.ReadTimeout("Timeout")
caller = Caller(header={})
response = caller.put("https://api.example.com/data/1", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Timeout (20s) during 'put'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 300
assert "Timeout (20s) during 'put'" in response.message
assert response.action == "put"
assert response.url == "https://api.example.com/data/1"
class TestCallerPatch:
@@ -345,7 +370,8 @@ class TestCallerPatch:
headers={"Content-Type": "application/json"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.patch')
@@ -364,16 +390,18 @@ class TestCallerPatch:
assert mock_patch.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.patch')
def test_patch_connection_error_returns_none(self, mock_patch: Mock, capsys: Any):
"""Test PATCH request connection error returns None"""
def test_patch_connection_error_returns_none(self, mock_patch: Mock):
"""Test PATCH request connection error returns ErrorResponse"""
mock_patch.side_effect = requests.exceptions.ConnectionError("Connection failed")
caller = Caller(header={})
response = caller.patch("https://api.example.com/data/1", data={"test": "data"})
assert response is None
captured = capsys.readouterr()
assert "Connection error during 'patch'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 400
assert "Connection error during 'patch'" in response.message
assert response.action == "patch"
assert response.url == "https://api.example.com/data/1"
class TestCallerDelete:
@@ -396,7 +424,8 @@ class TestCallerDelete:
headers={"Authorization": "Bearer token"},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.delete')
@@ -414,16 +443,18 @@ class TestCallerDelete:
assert mock_delete.call_args[1]["params"] == params
@patch('corelibs.requests_handling.caller.requests.delete')
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock, capsys: Any):
"""Test DELETE request with invalid URL schema returns None"""
def test_delete_invalid_schema_returns_none(self, mock_delete: Mock):
"""Test DELETE request with invalid URL schema returns ErrorResponse"""
mock_delete.side_effect = requests.exceptions.InvalidSchema("Invalid URL")
caller = Caller(header={})
response = caller.delete("invalid://example.com/data/1")
assert response is None
captured = capsys.readouterr()
assert "Invalid URL during 'delete'" in captured.out
assert isinstance(response, ErrorResponse)
assert response.code == 200
assert "Invalid URL during 'delete'" in response.message
assert response.action == "delete"
assert response.url == "invalid://example.com/data/1"
class TestCallerParametrized:
@@ -492,7 +523,7 @@ class TestCallerParametrized:
])
@patch('corelibs.requests_handling.caller.requests.get')
def test_exception_handling(
self, mock_get: Mock, exception_class: type, expected_message: str, capsys: Any
self, mock_get: Mock, exception_class: type, expected_message: str
):
"""Test exception handling for all exception types"""
mock_get.side_effect = exception_class("Test error")
@@ -500,9 +531,8 @@ class TestCallerParametrized:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is None
captured = capsys.readouterr()
assert expected_message in captured.out
assert isinstance(response, ErrorResponse)
assert expected_message in response.message
class TestCallerIntegration:
@@ -599,7 +629,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.post')
@@ -659,7 +690,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
@patch('corelibs.requests_handling.caller.requests.get')
@@ -679,7 +711,8 @@ class TestCallerEdgeCases:
headers={},
timeout=20,
verify=True,
proxies=None
proxies=None,
cert=None
)
def test_timeout_zero(self):
@@ -730,9 +763,10 @@ class TestCallerProxyHandling:
mock_response = Mock(spec=requests.Response)
mock_get.return_value = mock_response
proxy = {
"http": "http://proxy.example.com:8080",
"https": "https://proxy.example.com:8080"
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.get("https://api.example.com/data")
@@ -746,9 +780,10 @@ class TestCallerProxyHandling:
mock_response = Mock(spec=requests.Response)
mock_post.return_value = mock_response
proxy = {
"http": "http://user:pass@proxy.example.com:8080",
"https": "https://user:pass@proxy.example.com:8080"
proxy: ProxyConfig = {
"type": "socks5",
"host": "proxy.example.com:8080",
"port": "8080"
}
caller = Caller(header={}, proxy=proxy)
caller.post("https://api.example.com/data", data={"test": "data"})
@@ -789,7 +824,7 @@ class TestCallerResponseHandling:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert not isinstance(response, ErrorResponse)
assert response.status_code == 200
assert response.text == "Success"
assert response.json() == {"status": "ok"}
@@ -805,7 +840,7 @@ class TestCallerResponseHandling:
caller = Caller(header={})
response = caller.get("https://api.example.com/data")
assert response is not None
assert not isinstance(response, ErrorResponse)
assert response.status_code == status_code

View File

@@ -1,516 +0,0 @@
"""
PyTest: string_handling/text_colors
"""
import pytest
from corelibs.string_handling.text_colors import Colors
class TestColorsInitialState:
"""Tests for Colors class initial state"""
def test_bold_initial_value(self):
"""Test that bold has correct ANSI code"""
assert Colors.bold == '\033[1m'
def test_underline_initial_value(self):
"""Test that underline has correct ANSI code"""
assert Colors.underline == '\033[4m'
def test_end_initial_value(self):
"""Test that end has correct ANSI code"""
assert Colors.end == '\033[0m'
def test_reset_initial_value(self):
"""Test that reset has correct ANSI code"""
assert Colors.reset == '\033[0m'
class TestColorsNormal:
"""Tests for normal color ANSI codes"""
def test_black_normal(self):
"""Test black color code"""
assert Colors.black == "\033[30m"
def test_red_normal(self):
"""Test red color code"""
assert Colors.red == "\033[31m"
def test_green_normal(self):
"""Test green color code"""
assert Colors.green == "\033[32m"
def test_yellow_normal(self):
"""Test yellow color code"""
assert Colors.yellow == "\033[33m"
def test_blue_normal(self):
"""Test blue color code"""
assert Colors.blue == "\033[34m"
def test_magenta_normal(self):
"""Test magenta color code"""
assert Colors.magenta == "\033[35m"
def test_cyan_normal(self):
"""Test cyan color code"""
assert Colors.cyan == "\033[36m"
def test_white_normal(self):
"""Test white color code"""
assert Colors.white == "\033[37m"
class TestColorsBold:
"""Tests for bold color ANSI codes"""
def test_black_bold(self):
"""Test black bold color code"""
assert Colors.black_bold == "\033[1;30m"
def test_red_bold(self):
"""Test red bold color code"""
assert Colors.red_bold == "\033[1;31m"
def test_green_bold(self):
"""Test green bold color code"""
assert Colors.green_bold == "\033[1;32m"
def test_yellow_bold(self):
"""Test yellow bold color code"""
assert Colors.yellow_bold == "\033[1;33m"
def test_blue_bold(self):
"""Test blue bold color code"""
assert Colors.blue_bold == "\033[1;34m"
def test_magenta_bold(self):
"""Test magenta bold color code"""
assert Colors.magenta_bold == "\033[1;35m"
def test_cyan_bold(self):
"""Test cyan bold color code"""
assert Colors.cyan_bold == "\033[1;36m"
def test_white_bold(self):
"""Test white bold color code"""
assert Colors.white_bold == "\033[1;37m"
class TestColorsBright:
"""Tests for bright color ANSI codes"""
def test_black_bright(self):
"""Test black bright color code"""
assert Colors.black_bright == '\033[90m'
def test_red_bright(self):
"""Test red bright color code"""
assert Colors.red_bright == '\033[91m'
def test_green_bright(self):
"""Test green bright color code"""
assert Colors.green_bright == '\033[92m'
def test_yellow_bright(self):
"""Test yellow bright color code"""
assert Colors.yellow_bright == '\033[93m'
def test_blue_bright(self):
"""Test blue bright color code"""
assert Colors.blue_bright == '\033[94m'
def test_magenta_bright(self):
"""Test magenta bright color code"""
assert Colors.magenta_bright == '\033[95m'
def test_cyan_bright(self):
"""Test cyan bright color code"""
assert Colors.cyan_bright == '\033[96m'
def test_white_bright(self):
"""Test white bright color code"""
assert Colors.white_bright == '\033[97m'
class TestColorsDisable:
"""Tests for Colors.disable() method"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_disable_bold_and_underline(self):
"""Test that disable() sets bold and underline to empty strings"""
Colors.disable()
assert Colors.bold == ''
assert Colors.underline == ''
def test_disable_end_and_reset(self):
"""Test that disable() sets end and reset to empty strings"""
Colors.disable()
assert Colors.end == ''
assert Colors.reset == ''
def test_disable_normal_colors(self):
"""Test that disable() sets all normal colors to empty strings"""
Colors.disable()
assert Colors.black == ''
assert Colors.red == ''
assert Colors.green == ''
assert Colors.yellow == ''
assert Colors.blue == ''
assert Colors.magenta == ''
assert Colors.cyan == ''
assert Colors.white == ''
def test_disable_bold_colors(self):
"""Test that disable() sets all bold colors to empty strings"""
Colors.disable()
assert Colors.black_bold == ''
assert Colors.red_bold == ''
assert Colors.green_bold == ''
assert Colors.yellow_bold == ''
assert Colors.blue_bold == ''
assert Colors.magenta_bold == ''
assert Colors.cyan_bold == ''
assert Colors.white_bold == ''
def test_disable_bright_colors(self):
"""Test that disable() sets all bright colors to empty strings"""
Colors.disable()
assert Colors.black_bright == ''
assert Colors.red_bright == ''
assert Colors.green_bright == ''
assert Colors.yellow_bright == ''
assert Colors.blue_bright == ''
assert Colors.magenta_bright == ''
assert Colors.cyan_bright == ''
assert Colors.white_bright == ''
def test_disable_all_colors_at_once(self):
"""Test that all color attributes are empty after disable()"""
Colors.disable()
# Check that all public attributes are empty strings
for attr in dir(Colors):
if not attr.startswith('_') and attr not in ['disable', 'reset_colors']:
assert getattr(Colors, attr) == '', f"{attr} should be empty after disable()"
class TestColorsResetColors:
"""Tests for Colors.reset_colors() method"""
def setup_method(self):
"""Disable colors before each test"""
Colors.disable()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_reset_bold_and_underline(self):
"""Test that reset_colors() restores bold and underline"""
Colors.reset_colors()
assert Colors.bold == '\033[1m'
assert Colors.underline == '\033[4m'
def test_reset_end_and_reset(self):
"""Test that reset_colors() restores end and reset"""
Colors.reset_colors()
assert Colors.end == '\033[0m'
assert Colors.reset == '\033[0m'
def test_reset_normal_colors(self):
"""Test that reset_colors() restores all normal colors"""
Colors.reset_colors()
assert Colors.black == "\033[30m"
assert Colors.red == "\033[31m"
assert Colors.green == "\033[32m"
assert Colors.yellow == "\033[33m"
assert Colors.blue == "\033[34m"
assert Colors.magenta == "\033[35m"
assert Colors.cyan == "\033[36m"
assert Colors.white == "\033[37m"
def test_reset_bold_colors(self):
"""Test that reset_colors() restores all bold colors"""
Colors.reset_colors()
assert Colors.black_bold == "\033[1;30m"
assert Colors.red_bold == "\033[1;31m"
assert Colors.green_bold == "\033[1;32m"
assert Colors.yellow_bold == "\033[1;33m"
assert Colors.blue_bold == "\033[1;34m"
assert Colors.magenta_bold == "\033[1;35m"
assert Colors.cyan_bold == "\033[1;36m"
assert Colors.white_bold == "\033[1;37m"
def test_reset_bright_colors(self):
"""Test that reset_colors() restores all bright colors"""
Colors.reset_colors()
assert Colors.black_bright == '\033[90m'
assert Colors.red_bright == '\033[91m'
assert Colors.green_bright == '\033[92m'
assert Colors.yellow_bright == '\033[93m'
assert Colors.blue_bright == '\033[94m'
assert Colors.magenta_bright == '\033[95m'
assert Colors.cyan_bright == '\033[96m'
assert Colors.white_bright == '\033[97m'
class TestColorsDisableAndReset:
"""Tests for disable and reset cycle"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_disable_then_reset_cycle(self):
"""Test that colors can be disabled and then reset multiple times"""
# Initial state
original_red = Colors.red
# Disable
Colors.disable()
assert Colors.red == ''
# Reset
Colors.reset_colors()
assert Colors.red == original_red
# Disable again
Colors.disable()
assert Colors.red == ''
# Reset again
Colors.reset_colors()
assert Colors.red == original_red
def test_multiple_disables(self):
"""Test that calling disable() multiple times is safe"""
Colors.disable()
Colors.disable()
Colors.disable()
assert Colors.red == ''
assert Colors.blue == ''
def test_multiple_resets(self):
"""Test that calling reset_colors() multiple times is safe"""
Colors.reset_colors()
Colors.reset_colors()
Colors.reset_colors()
assert Colors.red == "\033[31m"
assert Colors.blue == "\033[34m"
class TestColorsUsage:
"""Tests for practical usage of Colors class"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_colored_string_with_reset(self):
"""Test creating a colored string with reset"""
result = f"{Colors.red}Error{Colors.end}"
assert result == "\033[31mError\033[0m"
def test_bold_colored_string(self):
"""Test creating a bold colored string"""
result = f"{Colors.bold}{Colors.yellow}Warning{Colors.end}"
assert result == "\033[1m\033[33mWarning\033[0m"
def test_underline_colored_string(self):
"""Test creating an underlined colored string"""
result = f"{Colors.underline}{Colors.blue}Info{Colors.end}"
assert result == "\033[4m\033[34mInfo\033[0m"
def test_bold_underline_colored_string(self):
"""Test creating a bold and underlined colored string"""
result = f"{Colors.bold}{Colors.underline}{Colors.green}Success{Colors.end}"
assert result == "\033[1m\033[4m\033[32mSuccess\033[0m"
def test_multiple_colors_in_string(self):
"""Test using multiple colors in one string"""
result = f"{Colors.red}Red{Colors.end} {Colors.blue}Blue{Colors.end}"
assert result == "\033[31mRed\033[0m \033[34mBlue\033[0m"
def test_bright_color_usage(self):
"""Test using bright color variants"""
result = f"{Colors.cyan_bright}Bright Cyan{Colors.end}"
assert result == "\033[96mBright Cyan\033[0m"
def test_bold_color_shortcut(self):
"""Test using bold color shortcuts"""
result = f"{Colors.red_bold}Bold Red{Colors.end}"
assert result == "\033[1;31mBold Red\033[0m"
def test_disabled_colors_produce_plain_text(self):
"""Test that disabled colors produce plain text without ANSI codes"""
Colors.disable()
result = f"{Colors.red}Error{Colors.end}"
assert result == "Error"
assert "\033[" not in result
def test_disabled_bold_underline_produce_plain_text(self):
"""Test that disabled formatting produces plain text"""
Colors.disable()
result = f"{Colors.bold}{Colors.underline}{Colors.green}Success{Colors.end}"
assert result == "Success"
assert "\033[" not in result
class TestColorsPrivateAttributes:
"""Tests to ensure private attributes are not directly accessible"""
def test_private_bold_not_accessible(self):
"""Test that __BOLD is private"""
with pytest.raises(AttributeError):
_ = Colors.__BOLD
def test_private_colors_not_accessible(self):
"""Test that private color attributes are not accessible"""
with pytest.raises(AttributeError):
_ = Colors.__RED
with pytest.raises(AttributeError):
_ = Colors.__GREEN
# Parametrized tests
@pytest.mark.parametrize("color_attr,expected_code", [
("black", "\033[30m"),
("red", "\033[31m"),
("green", "\033[32m"),
("yellow", "\033[33m"),
("blue", "\033[34m"),
("magenta", "\033[35m"),
("cyan", "\033[36m"),
("white", "\033[37m"),
])
def test_normal_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for normal colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr,expected_code", [
("black_bold", "\033[1;30m"),
("red_bold", "\033[1;31m"),
("green_bold", "\033[1;32m"),
("yellow_bold", "\033[1;33m"),
("blue_bold", "\033[1;34m"),
("magenta_bold", "\033[1;35m"),
("cyan_bold", "\033[1;36m"),
("white_bold", "\033[1;37m"),
])
def test_bold_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for bold colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr,expected_code", [
("black_bright", '\033[90m'),
("red_bright", '\033[91m'),
("green_bright", '\033[92m'),
("yellow_bright", '\033[93m'),
("blue_bright", '\033[94m'),
("magenta_bright", '\033[95m'),
("cyan_bright", '\033[96m'),
("white_bright", '\033[97m'),
])
def test_bright_colors_parametrized(color_attr: str, expected_code: str):
"""Parametrized test for bright colors"""
Colors.reset_colors()
assert getattr(Colors, color_attr) == expected_code
@pytest.mark.parametrize("color_attr", [
"bold", "underline", "end", "reset",
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
"black_bold", "red_bold", "green_bold", "yellow_bold",
"blue_bold", "magenta_bold", "cyan_bold", "white_bold",
"black_bright", "red_bright", "green_bright", "yellow_bright",
"blue_bright", "magenta_bright", "cyan_bright", "white_bright",
])
def test_disable_all_attributes_parametrized(color_attr: str):
"""Parametrized test that all color attributes are disabled"""
Colors.reset_colors()
Colors.disable()
assert getattr(Colors, color_attr) == ''
@pytest.mark.parametrize("color_attr", [
"bold", "underline", "end", "reset",
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
"black_bold", "red_bold", "green_bold", "yellow_bold",
"blue_bold", "magenta_bold", "cyan_bold", "white_bold",
"black_bright", "red_bright", "green_bright", "yellow_bright",
"blue_bright", "magenta_bright", "cyan_bright", "white_bright",
])
def test_reset_all_attributes_parametrized(color_attr: str):
"""Parametrized test that all color attributes are reset"""
Colors.disable()
Colors.reset_colors()
assert getattr(Colors, color_attr) != ''
assert '\033[' in getattr(Colors, color_attr)
# Edge case tests
class TestColorsEdgeCases:
"""Tests for edge cases and special scenarios"""
def setup_method(self):
"""Reset colors before each test"""
Colors.reset_colors()
def teardown_method(self):
"""Reset colors after each test"""
Colors.reset_colors()
def test_colors_class_is_not_instantiable(self):
"""Test that Colors class can be instantiated (it's not abstract)"""
# The class uses static methods, but can be instantiated
instance = Colors()
assert isinstance(instance, Colors)
def test_static_methods_work_on_instance(self):
"""Test that static methods work when called on instance"""
instance = Colors()
instance.disable()
assert Colors.red == ''
instance.reset_colors()
assert Colors.red == "\033[31m"
def test_concatenation_of_multiple_effects(self):
"""Test concatenating multiple color effects"""
result = f"{Colors.bold}{Colors.underline}{Colors.red_bright}Test{Colors.reset}"
assert "\033[1m" in result # bold
assert "\033[4m" in result # underline
assert "\033[91m" in result # red bright
assert "\033[0m" in result # reset
def test_empty_string_with_colors(self):
"""Test applying colors to empty string"""
result = f"{Colors.red}{Colors.end}"
assert result == "\033[31m\033[0m"
def test_nested_color_changes(self):
"""Test nested color changes in string"""
result = f"{Colors.red}Red {Colors.blue}Blue{Colors.end} Red again{Colors.end}"
assert result == "\033[31mRed \033[34mBlue\033[0m Red again\033[0m"
# __END__

View File

@@ -1,3 +0,0 @@
"""
var_handling tests
"""

View File

@@ -1,546 +0,0 @@
"""
var_handling.enum_base tests
"""
from typing import Any
import pytest
from corelibs.var_handling.enum_base import EnumBase
class SampleBlock(EnumBase):
"""Sample block enum for testing purposes"""
BLOCK_A = "block_a"
BLOCK_B = "block_b"
HAS_NUM = 5
HAS_FLOAT = 3.14
LEGACY_KEY = "legacy_value"
class SimpleEnum(EnumBase):
"""Simple enum with string values"""
OPTION_ONE = "one"
OPTION_TWO = "two"
OPTION_THREE = "three"
class NumericEnum(EnumBase):
"""Enum with only numeric values"""
FIRST = 1
SECOND = 2
THIRD = 3
class TestEnumBaseLookupKey:
"""Test cases for lookup_key class method"""
def test_lookup_key_valid_uppercase(self):
"""Test lookup_key with valid uppercase key"""
result = SampleBlock.lookup_key("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_key_valid_lowercase(self):
"""Test lookup_key with valid lowercase key (should convert to uppercase)"""
result = SampleBlock.lookup_key("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_valid_mixed_case(self):
"""Test lookup_key with mixed case key"""
result = SampleBlock.lookup_key("BlOcK_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
def test_lookup_key_with_numeric_enum(self):
"""Test lookup_key with numeric enum member"""
result = SampleBlock.lookup_key("HAS_NUM")
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_lookup_key_legacy_colon_replacement(self):
"""Test lookup_key with legacy colon format (converts : to ___)"""
# This assumes the enum has a key that might be accessed with legacy format
# Should convert : to ___ and look up LEGACY___KEY
# Since we don't have this key, we test the behavior with a valid conversion
# Let's test with a known key that would work
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("BLOCK:A") # Should fail as BLOCK___A doesn't exist
def test_lookup_key_invalid_key(self):
"""Test lookup_key with invalid key"""
with pytest.raises(ValueError, match="Invalid key: NONEXISTENT"):
SampleBlock.lookup_key("NONEXISTENT")
def test_lookup_key_empty_string(self):
"""Test lookup_key with empty string"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("")
def test_lookup_key_with_special_characters(self):
"""Test lookup_key with special characters that might cause AttributeError"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("@#$%")
def test_lookup_key_numeric_string(self):
"""Test lookup_key with numeric string that isn't a key"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key("123")
class TestEnumBaseLookupValue:
"""Test cases for lookup_value class method"""
def test_lookup_value_valid_string(self):
"""Test lookup_value with valid string value"""
result = SampleBlock.lookup_value("block_a")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_lookup_value_valid_integer(self):
"""Test lookup_value with valid integer value"""
result = SampleBlock.lookup_value(5)
assert result == SampleBlock.HAS_NUM
assert result.name == "HAS_NUM"
assert result.value == 5
def test_lookup_value_valid_float(self):
"""Test lookup_value with valid float value"""
result = SampleBlock.lookup_value(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.name == "HAS_FLOAT"
assert result.value == 3.14
def test_lookup_value_invalid_string(self):
"""Test lookup_value with invalid string value"""
with pytest.raises(ValueError, match="Invalid value: nonexistent"):
SampleBlock.lookup_value("nonexistent")
def test_lookup_value_invalid_integer(self):
"""Test lookup_value with invalid integer value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.lookup_value(999)
def test_lookup_value_case_sensitive(self):
"""Test that lookup_value is case-sensitive for string values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value("BLOCK_A") # Value is "block_a", not "BLOCK_A"
class TestEnumBaseFromAny:
"""Test cases for from_any class method"""
def test_from_any_with_enum_instance(self):
"""Test from_any with an enum instance (should return as-is)"""
enum_instance = SampleBlock.BLOCK_A
result = SampleBlock.from_any(enum_instance)
assert result is enum_instance
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_key(self):
"""Test from_any with string that matches a key"""
result = SampleBlock.from_any("BLOCK_A")
assert result == SampleBlock.BLOCK_A
assert result.name == "BLOCK_A"
assert result.value == "block_a"
def test_from_any_with_string_as_key_lowercase(self):
"""Test from_any with lowercase string key"""
result = SampleBlock.from_any("block_a")
# Should first try as key (convert to uppercase and find BLOCK_A)
assert result == SampleBlock.BLOCK_A
def test_from_any_with_string_as_value(self):
"""Test from_any with string that only matches a value"""
# Use a value that isn't also a valid key
result = SampleBlock.from_any("block_b")
# Should try key first (fail), then value (succeed)
assert result == SampleBlock.BLOCK_B
assert result.value == "block_b"
def test_from_any_with_integer(self):
"""Test from_any with integer value"""
result = SampleBlock.from_any(5)
assert result == SampleBlock.HAS_NUM
assert result.value == 5
def test_from_any_with_float(self):
"""Test from_any with float value"""
result = SampleBlock.from_any(3.14)
assert result == SampleBlock.HAS_FLOAT
assert result.value == 3.14
def test_from_any_with_invalid_string(self):
"""Test from_any with string that doesn't match key or value"""
with pytest.raises(ValueError, match="Could not find as key or value: invalid_string"):
SampleBlock.from_any("invalid_string")
def test_from_any_with_invalid_integer(self):
"""Test from_any with integer that doesn't match any value"""
with pytest.raises(ValueError, match="Invalid value: 999"):
SampleBlock.from_any(999)
def test_from_any_string_key_priority(self):
"""Test that from_any tries key lookup before value for strings"""
# Create an enum where a value matches another key
class AmbiguousEnum(EnumBase):
KEY_A = "key_b" # Value is the name of another key
KEY_B = "value_b"
# When we look up "KEY_B", it should find it as a key, not as value "key_b"
result = AmbiguousEnum.from_any("KEY_B")
assert result == AmbiguousEnum.KEY_B
assert result.value == "value_b"
class TestEnumBaseToValue:
"""Test cases for to_value instance method"""
def test_to_value_string_value(self):
"""Test to_value with string enum value"""
result = SampleBlock.BLOCK_A.to_value()
assert result == "block_a"
assert isinstance(result, str)
def test_to_value_integer_value(self):
"""Test to_value with integer enum value"""
result = SampleBlock.HAS_NUM.to_value()
assert result == 5
assert isinstance(result, int)
def test_to_value_float_value(self):
"""Test to_value with float enum value"""
result = SampleBlock.HAS_FLOAT.to_value()
assert result == 3.14
assert isinstance(result, float)
def test_to_value_equals_value_attribute(self):
"""Test that to_value returns the same as .value"""
enum_instance = SampleBlock.BLOCK_A
assert enum_instance.to_value() == enum_instance.value
class TestEnumBaseToLowerCase:
"""Test cases for to_lower_case instance method"""
def test_to_lower_case_uppercase_name(self):
"""Test to_lower_case with uppercase enum name"""
result = SampleBlock.BLOCK_A.to_lower_case()
assert result == "block_a"
assert isinstance(result, str)
def test_to_lower_case_mixed_name(self):
"""Test to_lower_case with name containing underscores"""
result = SampleBlock.HAS_NUM.to_lower_case()
assert result == "has_num"
def test_to_lower_case_consistency(self):
"""Test that to_lower_case always returns lowercase"""
for member in SampleBlock:
result = member.to_lower_case()
assert result == result.lower()
assert result == member.name.lower()
class TestEnumBaseStrMethod:
"""Test cases for __str__ magic method"""
def test_str_returns_name(self):
"""Test that str() returns the enum name"""
result = str(SampleBlock.BLOCK_A)
assert result == "BLOCK_A"
assert result == SampleBlock.BLOCK_A.name
def test_str_all_members(self):
"""Test str() for all enum members"""
for member in SampleBlock:
result = str(member)
assert result == member.name
assert isinstance(result, str)
def test_str_in_formatting(self):
"""Test that str works in string formatting"""
formatted = f"Enum: {SampleBlock.BLOCK_A}"
assert formatted == "Enum: BLOCK_A"
def test_str_vs_repr(self):
"""Test difference between str and repr"""
enum_instance = SampleBlock.BLOCK_A
str_result = str(enum_instance)
repr_result = repr(enum_instance)
assert str_result == "BLOCK_A"
# repr should include class name
assert "SampleBlock" in repr_result
# Parametrized tests for comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("key,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("BLOCK_B", SampleBlock.BLOCK_B),
("HAS_NUM", SampleBlock.HAS_NUM),
("has_num", SampleBlock.HAS_NUM),
("HAS_FLOAT", SampleBlock.HAS_FLOAT),
])
def test_lookup_key_parametrized(self, key: str, expected_member: EnumBase):
"""Test lookup_key with various valid keys"""
result = SampleBlock.lookup_key(key)
assert result == expected_member
@pytest.mark.parametrize("value,expected_member", [
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
("legacy_value", SampleBlock.LEGACY_KEY),
])
def test_lookup_value_parametrized(self, value: Any, expected_member: EnumBase):
"""Test lookup_value with various valid values"""
result = SampleBlock.lookup_value(value)
assert result == expected_member
@pytest.mark.parametrize("input_any,expected_member", [
("BLOCK_A", SampleBlock.BLOCK_A),
("block_a", SampleBlock.BLOCK_A),
("block_b", SampleBlock.BLOCK_B),
(5, SampleBlock.HAS_NUM),
(3.14, SampleBlock.HAS_FLOAT),
(SampleBlock.BLOCK_A, SampleBlock.BLOCK_A), # Pass enum instance
])
def test_from_any_parametrized(self, input_any: Any, expected_member: EnumBase):
"""Test from_any with various valid inputs"""
result = SampleBlock.from_any(input_any)
assert result == expected_member
@pytest.mark.parametrize("invalid_key", [
"NONEXISTENT",
"invalid",
"123",
"",
"BLOCK_C",
])
def test_lookup_key_invalid_parametrized(self, invalid_key: str):
"""Test lookup_key with various invalid keys"""
with pytest.raises(ValueError, match="Invalid key"):
SampleBlock.lookup_key(invalid_key)
@pytest.mark.parametrize("invalid_value", [
"nonexistent",
999,
-1,
0.0,
"BLOCK_A", # This is a key name, not a value
])
def test_lookup_value_invalid_parametrized(self, invalid_value: Any):
"""Test lookup_value with various invalid values"""
with pytest.raises(ValueError, match="Invalid value"):
SampleBlock.lookup_value(invalid_value)
# Edge cases and special scenarios
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_enum_with_single_member(self):
"""Test EnumBase with only one member"""
class SingleEnum(EnumBase):
ONLY_ONE = "single"
result = SingleEnum.from_any("ONLY_ONE")
assert result == SingleEnum.ONLY_ONE
assert result.to_value() == "single"
def test_enum_iteration(self):
"""Test iterating over enum members"""
members = list(SampleBlock)
assert len(members) == 5
assert SampleBlock.BLOCK_A in members
assert SampleBlock.BLOCK_B in members
assert SampleBlock.HAS_NUM in members
def test_enum_membership(self):
"""Test checking membership in enum"""
assert SampleBlock.BLOCK_A in SampleBlock
assert SampleBlock.HAS_NUM in SampleBlock
def test_enum_comparison(self):
"""Test comparing enum members"""
assert SampleBlock.BLOCK_A == SampleBlock.BLOCK_A
assert SampleBlock.BLOCK_A != SampleBlock.BLOCK_B
assert SampleBlock.from_any("BLOCK_A") == SampleBlock.BLOCK_A
def test_enum_identity(self):
"""Test enum member identity"""
member1 = SampleBlock.BLOCK_A
member2 = SampleBlock.lookup_key("BLOCK_A")
member3 = SampleBlock.from_any("BLOCK_A")
assert member1 is member2
assert member1 is member3
assert member2 is member3
def test_different_enum_classes(self):
"""Test that different enum classes are distinct"""
# Even if they have same keys/values, they're different
class OtherEnum(EnumBase):
BLOCK_A = "block_a"
result1 = SampleBlock.from_any("BLOCK_A")
result2 = OtherEnum.from_any("BLOCK_A")
assert result1 != result2
assert not isinstance(result1, type(result2))
def test_numeric_enum_operations(self):
"""Test operations specific to numeric enums"""
assert NumericEnum.FIRST.to_value() == 1
assert NumericEnum.SECOND.to_value() == 2
assert NumericEnum.THIRD.to_value() == 3
# Test from_any with integers
assert NumericEnum.from_any(1) == NumericEnum.FIRST
assert NumericEnum.from_any(2) == NumericEnum.SECOND
def test_mixed_value_types_in_same_enum(self):
"""Test enum with mixed value types"""
# SampleBlock already has mixed types (strings, int, float)
assert isinstance(SampleBlock.BLOCK_A.to_value(), str)
assert isinstance(SampleBlock.HAS_NUM.to_value(), int)
assert isinstance(SampleBlock.HAS_FLOAT.to_value(), float)
def test_from_any_chained_calls(self):
"""Test that from_any can be chained (idempotent)"""
result1 = SampleBlock.from_any("BLOCK_A")
result2 = SampleBlock.from_any(result1)
result3 = SampleBlock.from_any(result2)
assert result1 == result2 == result3
assert result1 is result2 is result3
# Integration tests
class TestIntegration:
"""Integration tests combining multiple methods"""
def test_round_trip_key_lookup(self):
"""Test round-trip from key to enum and back"""
original_key = "BLOCK_A"
enum_member = SampleBlock.lookup_key(original_key)
result_name = str(enum_member)
assert result_name == original_key
def test_round_trip_value_lookup(self):
"""Test round-trip from value to enum and back"""
original_value = "block_a"
enum_member = SampleBlock.lookup_value(original_value)
result_value = enum_member.to_value()
assert result_value == original_value
def test_from_any_workflow(self):
"""Test realistic workflow using from_any"""
# Simulate receiving various types of input
inputs = [
"BLOCK_A", # Key as string
"block_b", # Value as string
5, # Numeric value
SampleBlock.HAS_FLOAT, # Already an enum
]
expected = [
SampleBlock.BLOCK_A,
SampleBlock.BLOCK_B,
SampleBlock.HAS_NUM,
SampleBlock.HAS_FLOAT,
]
for input_val, expected_val in zip(inputs, expected):
result = SampleBlock.from_any(input_val)
assert result == expected_val
def test_enum_in_dictionary(self):
"""Test using enum as dictionary key"""
enum_dict = {
SampleBlock.BLOCK_A: "Value A",
SampleBlock.BLOCK_B: "Value B",
SampleBlock.HAS_NUM: "Value Num",
}
assert enum_dict[SampleBlock.BLOCK_A] == "Value A"
block_b = SampleBlock.from_any("BLOCK_B")
assert isinstance(block_b, SampleBlock)
assert enum_dict[block_b] == "Value B"
def test_enum_in_set(self):
"""Test using enum in a set"""
enum_set = {SampleBlock.BLOCK_A, SampleBlock.BLOCK_B, SampleBlock.BLOCK_A}
assert len(enum_set) == 2 # BLOCK_A should be deduplicated
assert SampleBlock.BLOCK_A in enum_set
assert SampleBlock.from_any("BLOCK_B") in enum_set
# Real-world usage scenarios
class TestRealWorldScenarios:
"""Test real-world usage scenarios from enum_test.py"""
def test_original_enum_test_scenario(self):
"""Test the scenario from the original enum_test.py"""
# BLOCK A: {SampleBlock.from_any('BLOCK_A')}
result_a = SampleBlock.from_any('BLOCK_A')
assert result_a == SampleBlock.BLOCK_A
assert str(result_a) == "BLOCK_A"
# HAS NUM: {SampleBlock.from_any(5)}
result_num = SampleBlock.from_any(5)
assert result_num == SampleBlock.HAS_NUM
assert result_num.to_value() == 5
# DIRECT BLOCK: {SampleBlock.BLOCK_A.name} -> {SampleBlock.BLOCK_A.value}
assert SampleBlock.BLOCK_A.name == "BLOCK_A"
assert SampleBlock.BLOCK_A.value == "block_a"
def test_config_value_parsing(self):
"""Test parsing values from configuration (common use case)"""
# Simulate config values that might come as strings
config_values = ["OPTION_ONE", "option_two", "OPTION_THREE"]
results = [SimpleEnum.from_any(val) for val in config_values]
assert results[0] == SimpleEnum.OPTION_ONE
assert results[1] == SimpleEnum.OPTION_TWO
assert results[2] == SimpleEnum.OPTION_THREE
def test_api_response_mapping(self):
"""Test mapping API response values to enum"""
# Simulate API returning numeric codes
api_codes = [1, 2, 3]
results = [NumericEnum.from_any(code) for code in api_codes]
assert results[0] == NumericEnum.FIRST
assert results[1] == NumericEnum.SECOND
assert results[2] == NumericEnum.THIRD
def test_validation_with_error_handling(self):
"""Test validation with proper error handling"""
valid_input = "BLOCK_A"
invalid_input = "INVALID"
# Valid input should work
result = SampleBlock.from_any(valid_input)
assert result == SampleBlock.BLOCK_A
# Invalid input should raise ValueError
try:
SampleBlock.from_any(invalid_input)
assert False, "Should have raised ValueError"
except ValueError as e:
assert "Could not find as key or value" in str(e)
assert "INVALID" in str(e)

View File

@@ -1,241 +0,0 @@
"""
var helpers
"""
# ADDED 2025/7/11 Replace 'your_module' with actual module name
from typing import Any
import pytest
from corelibs.var_handling.var_helpers import is_int, is_float, str_to_bool
class TestIsInt:
"""Test cases for is_int function"""
def test_valid_integers(self):
"""Test with valid integer strings"""
assert is_int("123") is True
assert is_int("0") is True
assert is_int("-456") is True
assert is_int("+789") is True
assert is_int("000") is True
def test_invalid_integers(self):
"""Test with invalid integer strings"""
assert is_int("12.34") is False
assert is_int("abc") is False
assert is_int("12a") is False
assert is_int("") is False
assert is_int(" ") is False
assert is_int("12.0") is False
assert is_int("1e5") is False
def test_numeric_types(self):
"""Test with actual numeric types"""
assert is_int(123) is True
assert is_int(0) is True
assert is_int(-456) is True
assert is_int(12.34) is True # float can be converted to int
assert is_int(12.0) is True
def test_other_types(self):
"""Test with other data types"""
assert is_int(None) is False
assert is_int([]) is False
assert is_int({}) is False
assert is_int(True) is True # bool is subclass of int
assert is_int(False) is True
class TestIsFloat:
"""Test cases for is_float function"""
def test_valid_floats(self):
"""Test with valid float strings"""
assert is_float("12.34") is True
assert is_float("0.0") is True
assert is_float("-45.67") is True
assert is_float("+78.9") is True
assert is_float("123") is True # integers are valid floats
assert is_float("0") is True
assert is_float("1e5") is True
assert is_float("1.5e-10") is True
assert is_float("inf") is True
assert is_float("-inf") is True
assert is_float("nan") is True
def test_invalid_floats(self):
"""Test with invalid float strings"""
assert is_float("abc") is False
assert is_float("12.34.56") is False
assert is_float("12a") is False
assert is_float("") is False
assert is_float(" ") is False
assert is_float("12..34") is False
def test_numeric_types(self):
"""Test with actual numeric types"""
assert is_float(123) is True
assert is_float(12.34) is True
assert is_float(0) is True
assert is_float(-45.67) is True
def test_other_types(self):
"""Test with other data types"""
assert is_float(None) is False
assert is_float([]) is False
assert is_float({}) is False
assert is_float(True) is True # bool can be converted to float
assert is_float(False) is True
class TestStrToBool:
"""Test cases for str_to_bool function"""
def test_valid_true_strings(self):
"""Test with valid true strings"""
assert str_to_bool("True") is True
assert str_to_bool("true") is True
def test_valid_false_strings(self):
"""Test with valid false strings"""
assert str_to_bool("False") is False
assert str_to_bool("false") is False
def test_invalid_strings(self):
"""Test with invalid boolean strings"""
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("TRUE")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("FALSE")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("yes")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("no")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("1")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("0")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool(" True")
with pytest.raises(ValueError, match="Invalid boolean string"):
str_to_bool("True ")
def test_error_message_content(self):
"""Test that error messages contain the invalid input"""
with pytest.raises(ValueError) as exc_info:
str_to_bool("invalid")
assert "Invalid boolean string: invalid" in str(exc_info.value)
def test_case_sensitivity(self):
"""Test that function is case sensitive"""
with pytest.raises(ValueError):
str_to_bool("TRUE")
with pytest.raises(ValueError):
str_to_bool("True ") # with space
with pytest.raises(ValueError):
str_to_bool(" True") # with space
# Additional edge case tests
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_is_int_with_whitespace(self):
"""Test is_int with whitespace (should work due to int() behavior)"""
assert is_int(" 123 ") is True
assert is_int("\t456\n") is True
def test_is_float_with_whitespace(self):
"""Test is_float with whitespace (should work due to float() behavior)"""
assert is_float(" 12.34 ") is True
assert is_float("\t45.67\n") is True
def test_large_numbers(self):
"""Test with very large numbers"""
large_int = "123456789012345678901234567890"
assert is_int(large_int) is True
assert is_float(large_int) is True
def test_scientific_notation(self):
"""Test scientific notation"""
assert is_int("1e5") is False # int() doesn't handle scientific notation
assert is_float("1e5") is True
assert is_float("1.5e-10") is True
assert is_float("2E+3") is True
# Parametrized tests for more comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("value,expected", [
("123", True),
("0", True),
("-456", True),
("12.34", False),
("abc", False),
("", False),
(123, True),
(12.5, True),
(None, False),
])
def test_is_int_parametrized(self, value: Any, expected: bool):
"""Test"""
assert is_int(value) == expected
@pytest.mark.parametrize("value,expected", [
("12.34", True),
("123", True),
("0", True),
("-45.67", True),
("inf", True),
("nan", True),
("abc", False),
("", False),
(12.34, True),
(123, True),
(None, False),
])
def test_is_float_parametrized(self, value: Any, expected: bool):
"""test"""
assert is_float(value) == expected
@pytest.mark.parametrize("value,expected", [
("True", True),
("true", True),
("False", False),
("false", False),
])
def test_str_to_bool_valid_parametrized(self, value: Any, expected: bool):
"""test"""
assert str_to_bool(value) == expected
@pytest.mark.parametrize("invalid_value", [
"TRUE",
"FALSE",
"yes",
"no",
"1",
"0",
"",
" True",
"True ",
"invalid",
])
def test_str_to_bool_invalid_parametrized(self, invalid_value: Any):
"""test"""
with pytest.raises(ValueError):
str_to_bool(invalid_value)

444
uv.lock generated
View File

@@ -1,444 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "corelibs"
version = "0.31.1"
source = { editable = "." }
dependencies = [
{ name = "cryptography" },
{ name = "jmespath" },
{ name = "jsonpath-ng" },
{ name = "psutil" },
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "deepdiff" },
{ name = "pytest" },
{ name = "pytest-cov" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "jmespath", specifier = ">=1.0.1" },
{ name = "jsonpath-ng", specifier = ">=1.7.0" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "requests", specifier = ">=2.32.4" },
]
[package.metadata.requires-dev]
dev = [
{ name = "deepdiff", specifier = ">=8.6.1" },
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
]
[[package]]
name = "coverage"
version = "7.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" },
{ url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" },
{ url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" },
{ url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" },
{ url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" },
{ url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" },
{ url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" },
{ url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" },
{ url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" },
{ url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" },
{ url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" },
{ url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" },
{ url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" },
{ url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" },
{ url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" },
{ url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" },
{ url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" },
{ url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" },
{ url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" },
{ url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" },
{ url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" },
{ url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" },
{ url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" },
{ url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" },
{ url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" },
{ url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" },
{ url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" },
{ url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" },
{ url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" },
{ url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" },
{ url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" },
{ url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" },
{ url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" },
{ url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" },
{ url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" },
{ url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" },
{ url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" },
{ url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" },
{ url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" },
{ url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" },
{ url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
]
[[package]]
name = "deepdiff"
version = "8.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "orderly-set" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/76/36c9aab3d5c19a94091f7c6c6e784efca50d87b124bf026c36e94719f33c/deepdiff-8.6.1.tar.gz", hash = "sha256:ec56d7a769ca80891b5200ec7bd41eec300ced91ebcc7797b41eb2b3f3ff643a", size = 634054, upload-time = "2025-09-03T19:40:41.461Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl", hash = "sha256:ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b", size = 91378, upload-time = "2025-09-03T19:40:39.679Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "jmespath"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
[[package]]
name = "jsonpath-ng"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ply" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" },
]
[[package]]
name = "orderly-set"
version = "5.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/88/39c83c35d5e97cc203e9e77a4f93bf87ec89cf6a22ac4818fdcc65d66584/orderly_set-5.5.0.tar.gz", hash = "sha256:e87185c8e4d8afa64e7f8160ee2c542a475b738bc891dc3f58102e654125e6ce", size = 27414, upload-time = "2025-07-10T20:10:55.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl", hash = "sha256:46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7", size = 13068, upload-time = "2025-07-10T20:10:54.377Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "ply"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" },
]
[[package]]
name = "psutil"
version = "7.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/ec/7b8e6b9b1d22708138630ef34c53ab2b61032c04f16adfdbb96791c8c70c/psutil-7.1.2.tar.gz", hash = "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018", size = 487424, upload-time = "2025-10-25T10:46:34.931Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/d9/b56cc9f883140ac10021a8c9b0f4e16eed1ba675c22513cdcbce3ba64014/psutil-7.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0cc5c6889b9871f231ed5455a9a02149e388fffcb30b607fb7a8896a6d95f22e", size = 238575, upload-time = "2025-10-25T10:46:38.728Z" },
{ url = "https://files.pythonhosted.org/packages/36/eb/28d22de383888deb252c818622196e709da98816e296ef95afda33f1c0a2/psutil-7.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8e9e77a977208d84aa363a4a12e0f72189d58bbf4e46b49aae29a2c6e93ef206", size = 239297, upload-time = "2025-10-25T10:46:41.347Z" },
{ url = "https://files.pythonhosted.org/packages/89/5d/220039e2f28cc129626e54d63892ab05c0d56a29818bfe7268dcb5008932/psutil-7.1.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d9623a5e4164d2220ecceb071f4b333b3c78866141e8887c072129185f41278", size = 280420, upload-time = "2025-10-25T10:46:44.122Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7a/286f0e1c167445b2ef4a6cbdfc8c59fdb45a5a493788950cf8467201dc73/psutil-7.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:364b1c10fe4ed59c89ec49e5f1a70da353b27986fa8233b4b999df4742a5ee2f", size = 283049, upload-time = "2025-10-25T10:46:47.095Z" },
{ url = "https://files.pythonhosted.org/packages/aa/cc/7eb93260794a42e39b976f3a4dde89725800b9f573b014fac142002a5c98/psutil-7.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f101ef84de7e05d41310e3ccbdd65a6dd1d9eed85e8aaf0758405d022308e204", size = 248713, upload-time = "2025-10-25T10:46:49.573Z" },
{ url = "https://files.pythonhosted.org/packages/ab/1a/0681a92b53366e01f0a099f5237d0c8a2f79d322ac589cccde5e30c8a4e2/psutil-7.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:20c00824048a95de67f00afedc7b08b282aa08638585b0206a9fb51f28f1a165", size = 244644, upload-time = "2025-10-25T10:46:51.924Z" },
{ url = "https://files.pythonhosted.org/packages/56/9e/f1c5c746b4ed5320952acd3002d3962fe36f30524c00ea79fdf954cc6779/psutil-7.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc", size = 238640, upload-time = "2025-10-25T10:46:54.089Z" },
{ url = "https://files.pythonhosted.org/packages/32/ee/fd26216a735395cc25c3899634e34aeb41fb1f3dbb44acc67d9e594be562/psutil-7.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e", size = 239303, upload-time = "2025-10-25T10:46:56.932Z" },
{ url = "https://files.pythonhosted.org/packages/3c/cd/7d96eaec4ef7742b845a9ce2759a2769ecce4ab7a99133da24abacbc9e41/psutil-7.1.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee", size = 281717, upload-time = "2025-10-25T10:46:59.116Z" },
{ url = "https://files.pythonhosted.org/packages/bc/1a/7f0b84bdb067d35fe7fade5fff888408688caf989806ce2d6dae08c72dd5/psutil-7.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7", size = 284575, upload-time = "2025-10-25T10:47:00.944Z" },
{ url = "https://files.pythonhosted.org/packages/de/05/7820ef8f7b275268917e0c750eada5834581206d9024ca88edce93c4b762/psutil-7.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31", size = 249491, upload-time = "2025-10-25T10:47:03.174Z" },
{ url = "https://files.pythonhosted.org/packages/db/9a/58de399c7cb58489f08498459ff096cd76b3f1ddc4f224ec2c5ef729c7d0/psutil-7.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582", size = 244880, upload-time = "2025-10-25T10:47:05.228Z" },
{ url = "https://files.pythonhosted.org/packages/ae/89/b9f8d47ddbc52d7301fc868e8224e5f44ed3c7f55e6d0f54ecaf5dd9ff5e/psutil-7.1.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814", size = 237244, upload-time = "2025-10-25T10:47:07.086Z" },
{ url = "https://files.pythonhosted.org/packages/c8/7a/8628c2f6b240680a67d73d8742bb9ff39b1820a693740e43096d5dcb01e5/psutil-7.1.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb", size = 238101, upload-time = "2025-10-25T10:47:09.523Z" },
{ url = "https://files.pythonhosted.org/packages/30/28/5e27f4d5a0e347f8e3cc16cd7d35533dbce086c95807f1f0e9cd77e26c10/psutil-7.1.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3", size = 258675, upload-time = "2025-10-25T10:47:11.082Z" },
{ url = "https://files.pythonhosted.org/packages/e5/5c/79cf60c9acf36d087f0db0f82066fca4a780e97e5b3a2e4c38209c03d170/psutil-7.1.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a", size = 260203, upload-time = "2025-10-25T10:47:13.226Z" },
{ url = "https://files.pythonhosted.org/packages/f7/03/0a464404c51685dcb9329fdd660b1721e076ccd7b3d97dee066bcc9ffb15/psutil-7.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91", size = 246714, upload-time = "2025-10-25T10:47:15.093Z" },
{ url = "https://files.pythonhosted.org/packages/6a/32/97ca2090f2f1b45b01b6aa7ae161cfe50671de097311975ca6eea3e7aabc/psutil-7.1.2-cp37-abi3-win_arm64.whl", hash = "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4", size = 243742, upload-time = "2025-10-25T10:47:17.302Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]