Compare commits

..

137 Commits

Author SHA1 Message Date
Clemens Schwaighofer
28527990e9 Move progres and script helpers to corelibs-progress and corelibs-script 2026-02-06 15:55:26 +09:00
Clemens Schwaighofer
b58a26f79a Move all string support functions to corelibs_strings and other modules 2026-02-06 08:49:44 +09:00
Clemens Schwaighofer
8bb4a202cd Move CSV handling to corelibs-csv 2026-02-05 10:48:57 +09:00
Clemens Schwaighofer
f265b55ef8 Move requests handling to corelibs_requests module 2026-02-04 14:55:39 +09:00
Clemens Schwaighofer
85063ea5df Move iterator handling functions to corelibs_iterator, corelibs_hash and corelibs_dump_data modules
Deprecate math helpers in favor of built-in math functions
2026-02-03 18:58:28 +09:00
Clemens Schwaighofer
31086fea53 Move json_handling to corelibs_json module 2026-02-03 14:03:17 +09:00
Clemens Schwaighofer
fd956095de Move SymmetricEncryption to corelibs_encryption module 2026-02-03 13:32:18 +09:00
Clemens Schwaighofer
a046d9f84c Move file handling to corelibs_file module 2026-02-03 11:42:57 +09:00
Clemens Schwaighofer
2e0d5aeb51 Move all debug handling into their own packages
dump data: corelibs_dump_data
stack trace: corelibs_stack_trace
profiling, timing, etc: corelibs_debug
2026-02-03 10:48:59 +09:00
Clemens Schwaighofer
28ab7c6f0c Move regex checks to corelibs_regex_checks module 2026-02-02 14:56:07 +09:00
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
Clemens Schwaighofer
cd06272b38 v0.31.1: fix dict_helper file name to dict_helpers 2025-10-27 10:42:45 +09:00
Clemens Schwaighofer
c5ab4352e3 Fix name dict_helper to dict_helpers
So we have the same name for everyhing
2025-10-27 10:40:12 +09:00
Clemens Schwaighofer
0da4a6b70a v0.31.0: Add tests, move files to final location 2025-10-27 10:29:47 +09:00
Clemens Schwaighofer
11c5f3387c README info update 2025-10-27 10:17:32 +09:00
Clemens Schwaighofer
3ed0171e17 Readme update 2025-10-27 10:09:27 +09:00
Clemens Schwaighofer
c7b38b0d70 Add ignore list for coverage (pytest), rename json default function to default_isoformat 2025-10-27 10:05:31 +09:00
Clemens Schwaighofer
caf0039de4 script handling and string handling 2025-10-24 21:19:41 +09:00
Clemens Schwaighofer
2637e1e42c Tests for requests handling 2025-10-24 19:00:07 +09:00
Clemens Schwaighofer
d0a1673965 Add pytest for logging 2025-10-24 18:33:25 +09:00
Clemens Schwaighofer
07e5d23f72 Add jmespath tests 2025-10-24 16:47:46 +09:00
Clemens Schwaighofer
fb4fdb6857 iterator tests added 2025-10-24 16:36:42 +09:00
Clemens Schwaighofer
d642a13b6e file handling tests, move progress to script handling
Progress is not only file, but process progress in a script
2025-10-24 16:07:47 +09:00
Clemens Schwaighofer
8967031f91 csv interface minor update to use the csv exceptions for errors 2025-10-24 15:45:09 +09:00
Clemens Schwaighofer
89caada4cc debug handling pytests added 2025-10-24 15:44:51 +09:00
Clemens Schwaighofer
b3616269bc csv writer to csv interface with reader class
But this is more for reference and should not be considered final
Missing things are like
- all values to private
- reader interface to parts
- value check for delimiter, quotechar, etc
2025-10-24 14:43:29 +09:00
Clemens Schwaighofer
4fa22813ce Add tests for settings loader 2025-10-24 14:19:05 +09:00
Clemens Schwaighofer
3ee3a0dce0 Tests for check_handling/regex_constants 2025-10-24 13:45:46 +09:00
Clemens Schwaighofer
1226721bc0 v0.30.0: add datetime and timestamp handling 2025-10-24 10:07:28 +09:00
Clemens Schwaighofer
a76eae0cc7 Add datetime helpers and move all time/date time datetime_handling folder
previous string_handling located datetime and timestamp files have been moved
to the datetime handling folder

Update readme file with more information about currently covered areas
2025-10-24 10:03:04 +09:00
Clemens Schwaighofer
53cf2a6f48 Add prepare_url_slash to string_helpers.py and tests
Function cleans up url paths (without domain) by ensuring they start with a single slash and removing double slashes.
2025-10-23 15:47:19 +09:00
Clemens Schwaighofer
fe69530b38 Add a simple add key entry to dictionary 2025-10-23 15:31:52 +09:00
Clemens Schwaighofer
bf83c1c394 v0.29.0: Add SQLite IO class 2025-10-23 15:24:17 +09:00
Clemens Schwaighofer
84ce43ab93 Add SQLite IO class
This is a very basic class without many helper functions added yet
Add to the CoreLibs so when we develop it further it can be used by all projects
2025-10-23 15:22:12 +09:00
Clemens Schwaighofer
5e0765ee24 Rename the enum_test to enum_base for the test run file 2025-10-23 14:32:52 +09:00
Clemens Schwaighofer
6edf9398b7 v0.28.0: Enum base class added 2025-10-23 13:48:57 +09:00
Clemens Schwaighofer
30bf9c1bcb Add Enum base class
A helper class for handling enum classes with various lookup helpers
2025-10-23 13:47:13 +09:00
Clemens Schwaighofer
0b59f3cc7a v0.27.0: add json replace content method 2025-10-23 13:22:19 +09:00
Clemens Schwaighofer
2544fad9ce Add json helper function json_replace
Function can replace content for a json path string in a dictionary
2025-10-23 13:20:40 +09:00
Clemens Schwaighofer
e579ef5834 v0.26.0: Add Symmetric Encryption 2025-10-23 11:48:52 +09:00
Clemens Schwaighofer
543e9766a1 Add symmetric encryption and tests 2025-10-23 11:47:41 +09:00
Clemens Schwaighofer
4c3611aba7 v0.25.1: add missing jmespath exception check 2025-10-09 16:43:53 +09:00
Clemens Schwaighofer
dadc14563a jmespath search check update 2025-10-09 16:42:41 +09:00
Clemens Schwaighofer
c1eda7305b jmespath search, catch JMESPathTypeError error
This error can happend if we search for a key and try to make a value compare and the key does not exist.
Perhaps also when they key should return a list
2025-10-09 16:39:54 +09:00
Clemens Schwaighofer
2f4e236350 v0.25.0: add create datetime iso format 2025-10-08 16:09:29 +09:00
Clemens Schwaighofer
b858936c68 Add test file for datetime helpers 2025-10-08 16:08:23 +09:00
Clemens Schwaighofer
78ce30283e Version update in uv.lock (merge from master) 2025-10-08 15:58:58 +09:00
Clemens Schwaighofer
f85fbb86af Add iso datetime create with time zone support
The time zone check is for short mappings is limited, it is recommended
to use full TZ names like "Europe/Berlin", "Asia/Tokyo", "America/New_York"
2025-10-08 15:57:57 +09:00
Clemens Schwaighofer
ed22105ec8 v0.24.4: Fix Zone info data in TimestampStrings class 2025-09-25 15:54:54 +09:00
Clemens Schwaighofer
7c5af588c7 Update the TimestampStrings zone info handling
time_zone is the string version of the time zone data
time_zone_zi is the ZoneInfo object of above
2025-09-25 15:53:26 +09:00
Clemens Schwaighofer
2690a285d9 v0.24.3: Pytest fixes 2025-09-25 15:38:29 +09:00
Clemens Schwaighofer
bb60a570d0 Change the TimestampStrings check to check for str instead of not ZoneInfo.
This fixes the pytest problem which threw:
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

during Mocking
2025-09-25 15:36:47 +09:00
Clemens Schwaighofer
ca0ab2d7d1 v0.24.2: TimestampString allows ZoneInfo object as zone name 2025-09-25 15:16:19 +09:00
Clemens Schwaighofer
38bae7fb46 TimestampStrings allows ZoneInfo object as time_zone parameter
So we can use pre-parsed data

Some tests for parsing settings, timestamp output
2025-09-25 15:14:40 +09:00
Clemens Schwaighofer
14466c3ff8 v0.24.1: allow negative timestamp convert to seconds, add pytests for this function 2025-09-24 15:27:15 +09:00
Clemens Schwaighofer
fe824f9fb4 Merge branch 'development' 2025-09-24 15:26:22 +09:00
Clemens Schwaighofer
ef5981b473 convert_to_seconds allow negative time strings and add pytests 2025-09-24 15:25:53 +09:00
Clemens Schwaighofer
7d1ee70cf6 v0.24.0: Add timestamp seconds to human readable 2025-09-19 10:25:44 +09:00
Clemens Schwaighofer
7c72d99619 add pytests for seconds to human readable convert 2025-09-19 10:17:36 +09:00
Clemens Schwaighofer
b32887a6d8 Add time in seconds convert to human readable format 2025-09-19 09:57:51 +09:00
Clemens Schwaighofer
37a197e7f1 v0.23.0: json dumps updates for functions, safe dict dump 2025-09-03 18:15:48 +09:00
Clemens Schwaighofer
74cb3d2c54 dump_data and new json_dumps
dump_data adds flag to dump without indent

json_dumps is dump_data like, but will be geared towards secure dump of dict to json for strage
2025-09-03 18:14:26 +09:00
Clemens Schwaighofer
d19abcabc7 v0.22.6: Empty settings loader config for just data load 2025-08-26 14:40:22 +09:00
101 changed files with 9423 additions and 3639 deletions

1
.gitignore vendored
View File

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

109
README.md
View File

@@ -1,27 +1,87 @@
# CoreLibs for Python
This is a pip package that can be installed into any project and covers the following pars
> [!warning]
> 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
- dump outputs for data
- json helpers for conten replace and output
- dump outputs for data for debugging
- progress printing
- string formatting, time creation, byte formatting
- Enum base class
- SQLite simple IO class
- Symmetric encryption
## Current list
- config_handling: simple INI config file data loader with check/convert/etc
- csv_handling: csv dict writer helper
- csv_interface: csv dict writer/reader helper
- 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
- 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)
- logging_handling: extend log and also error message handling
- requests_handling: requests wrapper for better calls with auth headers
- script_handling: pid lock file handling, abort timer
- string_handling: byte format, datetime format, hashing, string formats for numbrers, double byte string format, etc
- string_handling: byte format, datetime format, datetime compare, hashing, string formats for numbers, double byte string format, etc
- var_handling: var type checkers, enum base class
## Unfinished
- 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
- check_handling.regex_constants_compiled: corelibs-regex-checks
- check_handling.regex_constants: corelibs-regex-checks
- csv_handling.csv_interface: corelibs-csv
- datetime_handling.datetime_helpers: corelibs-datetime
- datetime_handling.timestamp_convert: corelibs-datetime
- datetime_handling.timestamp_strings: corelibs-datetime
- debug_handling.debug_helpers: corelibs-stack-trace
- debug_handling.dump_data: corelibs-dump-data
- debug_handling.profiling: corelibs-debug
- debug_handling.timer: corelibs-debug
- debug_handling.writeline: corelibs-debug
- encryption_handling.symmetrix_encryption: corelibs-encryption
- exceptions.csv_exceptions: orelibs-csv
- file_handling.file_bom_encoding: corelibs-file
- file_handling.file_crc: corelibs-file
- file_handling.file_handling: corelibs-file
- iterator_handling.data_search: corelibs-search
- iterator_handling.dict_helpers: corelibs-iterator
- iterator_handling.dict_mask: corelibs-dump-data
- iterator_handling.fingerprint: corelibs-hash
- iterator_handling.list_helpers: corelibs-iterator
- json_handling.jmespath_helper: corelibs-search
- json_handling.json_helper: corelibs-json
- math_handling.math_helpers: python.math
- requests_handling.auth_helpers: corelibs-requests
- requests_handling.caller: corelibs-requests
- script_handling.progress: corelibs-progress
- script_handling.script_helpers: corelibs-script
- string_handling.byte_helpers: corelibs-strings
- string_handling.double_byte_string_format: corelibs-double-byte-format
- string_handling.hash_helpers: corelibs-hash
- string_handling.string_helpers: corelibs-strings
- string_handling.text_colors: corelibs-text-colors
- var_handling.enum_base: corelibs-enum-base
- var_handling.var_helpers: corelibs-var
## UV setup
@@ -33,7 +93,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
@@ -41,15 +101,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
@@ -66,38 +126,15 @@ Get a coverate report
```sh
uv run pytest --cov=corelibs
uv run pytest --cov=corelibs --cov-report=term-missing
```
### Other tests
In the test-run folder usage and run tests are located
#### Progress
In the test-run folder usage and run tests are located, runt them below
```sh
uv run test-run/progress/progress_test.py
```
#### Double byte string format
```sh
uv run test-run/double_byte_string_format/double_byte_string_format.py
```
#### Strings helpers
```sh
uv run test-run/timestamp_strings/timestamp_strings.py
```
```sh
uv run test-run/string_handling/string_helpers.py
```
#### Log
```sh
uv run test-run/logging_handling/log.py
uv run test-run/<script>
```
## How to install in another project
@@ -105,7 +142,7 @@ uv run test-run/logging_handling/log.py
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,34 +1,63 @@
# MARK: Project info
[project]
name = "corelibs"
version = "0.22.5"
version = "0.48.0"
description = "Collection of utils for Python scripts"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"corelibs-csv>=1.0.0",
"corelibs-datetime>=1.0.1",
"corelibs-debug>=1.0.0",
"corelibs-double-byte-format>=1.0.0",
"corelibs-dump-data>=1.0.0",
"corelibs-encryption>=1.0.0",
"corelibs-enum-base>=1.0.0",
"corelibs-file>=1.0.0",
"corelibs-hash>=1.0.0",
"corelibs-iterator>=1.0.0",
"corelibs-json>=1.0.0",
"corelibs-progress>=1.0.0",
"corelibs-regex-checks>=1.0.0",
"corelibs-requests>=1.0.0",
"corelibs-script>=1.0.0",
"corelibs-search>=1.0.0",
"corelibs-stack-trace>=1.0.0",
"corelibs-strings>=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
@@ -60,3 +89,31 @@ 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/*",
"*/test_*.py",
"*/__init__.py"
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:"
]
exclude_also = [
"def __.*__\\(",
"def __.*\\(",
"def _.*\\(",
]

View File

@@ -3,8 +3,20 @@ List of regex compiled strings that can be used
"""
import re
from warnings import warn, deprecated
from corelibs_regex_checks.regex_constants import (
compile_re as compile_re_ng,
SUB_EMAIL_BASIC_REGEX as SUB_EMAIL_BASIC_REGEX_NG,
EMAIL_BASIC_REGEX as EMAIL_BASIC_REGEX_NG,
NAME_EMAIL_SIMPLE_REGEX as NAME_EMAIL_SIMPLE_REGEX_NG,
NAME_EMAIL_BASIC_REGEX as NAME_EMAIL_BASIC_REGEX_NG,
DOMAIN_WITH_LOCALHOST_REGEX as DOMAIN_WITH_LOCALHOST_REGEX_NG,
DOMAIN_WITH_LOCALHOST_PORT_REGEX as DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG,
DOMAIN_REGEX as DOMAIN_REGEX_NG
)
@deprecated("Use corelibs_regex_checks.regex_constants.compile_re instead")
def compile_re(reg: str) -> re.Pattern[str]:
"""
compile a regex with verbose flag
@@ -15,23 +27,25 @@ def compile_re(reg: str) -> re.Pattern[str]:
Returns:
re.Pattern[str] -- _description_
"""
return re.compile(reg, re.VERBOSE)
return compile_re_ng(reg)
# 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 = SUB_EMAIL_BASIC_REGEX_NG
EMAIL_BASIC_REGEX = EMAIL_BASIC_REGEX_NG
# name + email regex for email sending type like "foo bar" <email@mail.com>
NAME_EMAIL_SIMPLE_REGEX = NAME_EMAIL_SIMPLE_REGEX_NG
# name + email with the basic regex set
NAME_EMAIL_BASIC_REGEX = NAME_EMAIL_BASIC_REGEX_NG
# Domain regex with localhost
DOMAIN_WITH_LOCALHOST_REGEX: str = r"""
^(?:localhost|(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,})$
"""
DOMAIN_WITH_LOCALHOST_REGEX = DOMAIN_WITH_LOCALHOST_REGEX_NG
# domain regex with loclhost and optional port
DOMAIN_WITH_LOCALHOST_PORT_REGEX: str = r"""
^(?:localhost|(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,})(?::\d+)?$
"""
DOMAIN_WITH_LOCALHOST_PORT_REGEX = DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG
# Domain, no localhost
DOMAIN_REGEX: str = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(?:\.[A-Za-z0-9-]{1,63}(?<!-))*\.[A-Za-z]{2,}$"
DOMAIN_REGEX = DOMAIN_REGEX_NG
# At the module level, issue a deprecation warning
warn("Use corelibs_regex_checks.regex_constants instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -0,0 +1,27 @@
"""
List of regex compiled strings that can be used
"""
import warnings
from corelibs_regex_checks.regex_constants_compiled import (
COMPILED_EMAIL_BASIC_REGEX as COMPILED_EMAIL_BASIC_REGEX_NG,
COMPILED_NAME_EMAIL_SIMPLE_REGEX as COMPILED_NAME_EMAIL_SIMPLE_REGEX_NG,
COMPILED_NAME_EMAIL_BASIC_REGEX as COMPILED_NAME_EMAIL_BASIC_REGEX_NG,
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX as COMPILED_DOMAIN_WITH_LOCALHOST_REGEX_NG,
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX as COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG,
COMPILED_DOMAIN_REGEX as COMPILED_DOMAIN_REGEX_NG
)
# all above in compiled form
COMPILED_EMAIL_BASIC_REGEX = COMPILED_EMAIL_BASIC_REGEX_NG
COMPILED_NAME_EMAIL_SIMPLE_REGEX = COMPILED_NAME_EMAIL_SIMPLE_REGEX_NG
COMPILED_NAME_EMAIL_BASIC_REGEX = COMPILED_NAME_EMAIL_BASIC_REGEX_NG
COMPILED_DOMAIN_WITH_LOCALHOST_REGEX = COMPILED_DOMAIN_WITH_LOCALHOST_REGEX_NG
COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX = COMPILED_DOMAIN_WITH_LOCALHOST_PORT_REGEX_NG
COMPILED_DOMAIN_REGEX = COMPILED_DOMAIN_REGEX_NG
# At the module level, issue a deprecation warning
warnings.warn("Use corelibs_regex_checks.regex_constants_compiled instead", DeprecationWarning, stacklevel=2)
# __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

@@ -0,0 +1,39 @@
"""
Write to CSV file
- each class set is one file write with one header set
"""
from warnings import warn
from corelibs_csv.csv_interface import (
CsvReader as CoreLibsCsvReader, CsvWriter as CoreLibsCsvWriter,
ENCODING as CoreLibsEncoding,
ENCODING_UTF8_SIG as CoreLibsEncodingUtf8Sig,
DELIMITER as CoreLibsDelimiter,
QUOTECHAR as CoreLibsQuotechar,
QUOTING as CoreLibsQuoting
)
ENCODING = CoreLibsEncoding
ENCODING_UTF8_SIG = CoreLibsEncodingUtf8Sig
DELIMITER = CoreLibsDelimiter
QUOTECHAR = CoreLibsQuotechar
# type: _QuotingType
QUOTING = CoreLibsQuoting
class CsvWriter(CoreLibsCsvWriter):
"""
write to a CSV file
"""
class CsvReader(CoreLibsCsvReader):
"""
read from a CSV file
"""
warn("Use corelibs_csv.csv_interface instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -1,93 +0,0 @@
"""
Write to CSV file
- each class set is one file write with one header set
"""
from typing import Any
from pathlib import Path
from collections import Counter
import csv
class CsvWriter:
"""
write to a CSV file
"""
def __init__(
self,
path: Path,
file_name: str,
header: dict[str, str],
header_order: list[str] | None = None
):
self.path = path
self.file_name = file_name
# Key: index for write for the line dict, Values: header entries
self.header = header
self.csv_file_writer = self.__open_csv(header_order)
def __open_csv(self, header_order: list[str] | None) -> 'csv.DictWriter[str] | None':
"""
open csv file for writing, write headers
Note that if there is no header_order set we use the order in header dictionary
Arguments:
line {list[str] | None} -- optional dedicated header order
Returns:
csv.DictWriter[str] | None: _description_
"""
# if header order is set, make sure all header value fields exist
header_values = self.header.values()
if header_order is not None:
if Counter(header_values) != Counter(header_order):
print(
"header order does not match header values: "
f"{', '.join(header_values)} != {', '.join(header_order)}"
)
return None
header_values = header_order
# no duplicates
if len(header_values) != len(set(header_values)):
print(f"Header must have unique values only: {', '.join(header_values)}")
return None
try:
fp = open(
self.path.joinpath(self.file_name),
"w", encoding="utf-8"
)
csv_file_writer = csv.DictWriter(
fp,
fieldnames=header_values,
delimiter=",",
quotechar='"',
quoting=csv.QUOTE_MINIMAL,
)
csv_file_writer.writeheader()
return csv_file_writer
except OSError as err:
print("OS error:", err)
return None
def write_csv(self, line: dict[str, str]) -> bool:
"""
write member csv line
Arguments:
line {dict[str, str]} -- _description_
Returns:
bool -- _description_
"""
if self.csv_file_writer is None:
return False
csv_row: dict[str, Any] = {}
# only write entries that are in the header list
for key, value in self.header.items():
csv_row[value] = line[key]
self.csv_file_writer.writerow(csv_row)
return True
# __END__

View File

@@ -0,0 +1,235 @@
"""
Various string based date/time helpers
"""
from datetime import datetime, time
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
Arguments:
timestamp {float} -- _description_
Keyword Arguments:
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
Returns:
str -- _description_
"""
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
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
If not set or not valid gets local time,
if that is not possible get UTC
Keyword Arguments:
timezone_tz {str} -- _description_ (default: {''})
Returns:
ZoneInfo -- _description_
"""
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
Returns:
str -- _description_
"""
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
Arguments:
date {str} -- _description_
Returns:
bool -- _description_
"""
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,
shift_time_zone: bool = True
) -> datetime | None:
"""
Parse date string in multiple formats
will add time zone info if not None
on default it will change the TZ and time to the new time zone
if no TZ info is set in date_str, then localtime is assumed
Arguments:
date_str {str} -- _description_
Keyword Arguments:
timezone_tz {str | ZoneInfo | None} -- _description_ (default: {None})
shift_time_zone {bool} -- _description_ (default: {True})
Returns:
datetime | None -- _description_
"""
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
If the dates are equal then false will be returned
on error return None
Arguments:
date1_str {str} -- _description_
date2_str {str} -- _description_
Returns:
None | bool -- _description_
"""
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.
Handles potential parsing errors gracefully.
Args:
date_list (list): List of date strings in format '2025-08-06T16:17:39.747+09:00'
Returns:
str: The date string with the newest/latest date, or None if list is empty or all dates are invalid
"""
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.
Allowed are short (eg Mon) or long names (eg Monday).
Arguments:
dow_days {str} -- A comma-separated list of days or ranges (e.g., "Mon,Wed-Fri")
Raises:
ValueError: If the input format is invalid or if duplicate days are found.
Returns:
list[tuple[int, str]] -- A list of tuples containing the day index and name.
"""
# we have Sun twice because it can be 0 or 7
# Mon is 1 and Sun is 7, which is ISO standard
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.
Arguments:
time_str {str} -- The time range string to parse.
Raises:
ValueError: Invalid time block set
ValueError: Invalid time format
ValueError: Start time must be before end time
Returns:
tuple[time, time] -- start time, end time: leading zeros formattd
"""
try:
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
Args:
time1 (tuple): (start_time, end_time) for first range
time2 (tuple): (start_time, end_time) for second range
allow_touching (bool): If True, touching ranges (e.g., 8:00-10:00 and 10:00-12:00) are allowed
Returns:
bool: True if ranges overlap or connect (based on allow_touching)
"""
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)
Time format: "HH:MM" (24-hour format)
Arguments:
current_time {str} -- _description_
start_time {str} -- _description_
end_time {str} -- _description_
Returns:
bool -- _description_
"""
# Convert string times to time objects
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.
Arguments:
base_day {str} -- The day to start the week from (e.g., "Mon").
Returns:
dict[int, str] -- A dictionary mapping day numbers to day names.
"""
try:
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

@@ -0,0 +1,88 @@
"""
Convert timestamp strings with time units into seconds and vice versa.
"""
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
The following units are allowed
Y: 365 days
M: 30 days
d, h, m, s
Arguments:
time_string {str} -- _description_
Raises:
ValueError: _description_
Returns:
int -- _description_
"""
# skip out if this is a number of any type
# numbers will br made float, rounded and then converted to int
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")
Zero values are omitted.
milliseconds if requested are added as fractional part of seconds.
Supports negative values with "-" prefix
if not int or float, will return as is
Args:
seconds (float): Time in seconds (can be negative)
show_microseconds (bool): Whether to show microseconds precision
Returns:
str: Compact human readable time format
"""
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
for example if we have 1d 1s it would output 1d 0h 0m 1s
Milliseconds will be shown if set, and added with ms at the end
Negative values will be prefixed with "-"
if not int or float, will return as is
Arguments:
timestamp {float} -- _description_
Keyword Arguments:
show_micro {bool} -- _description_ (default: {True})
Returns:
str -- _description_
"""
return timestamp_convert.convert_timestamp(timestamp, show_microseconds)
# __END__

View File

@@ -0,0 +1,21 @@
"""
Current timestamp strings and time zones
"""
from warnings import deprecated
from zoneinfo import ZoneInfo
from corelibs_datetime import timestamp_strings
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):
super().__init__(time_zone)
# __END__

View File

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_stack_trace.stack 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

@@ -0,0 +1,214 @@
"""
SQLite DB::IO
Will be moved to the CoreLibs
also method names are subject to change
"""
# import gc
from pathlib import Path
from typing import Any, Literal, TYPE_CHECKING
import sqlite3
from corelibs_stack_trace.stack import call_stack
if TYPE_CHECKING:
from corelibs.logging_handling.log import Logger
class SQLiteIO():
"""Mini SQLite interface"""
def __init__(
self,
log: 'Logger',
db_name: str | Path,
autocommit: bool = False,
enable_fkey: bool = True,
row_factory: str | None = None
):
self.log = log
self.db_name = db_name
self.autocommit = autocommit
self.enable_fkey = enable_fkey
self.row_factory = row_factory
self.conn: sqlite3.Connection | None = self.db_connect()
# def __del__(self):
# self.db_close()
def db_connect(self) -> sqlite3.Connection | None:
"""
Connect to SQLite database, create if it doesn't exist
"""
try:
# Connect to database (creates if doesn't exist)
self.conn = sqlite3.connect(self.db_name, autocommit=self.autocommit)
self.conn.setconfig(sqlite3.SQLITE_DBCONFIG_ENABLE_FKEY, True)
# self.conn.execute("PRAGMA journal_mode=WAL")
# self.log.debug(f"Connected to database: {self.db_name}")
def dict_factory(cursor: sqlite3.Cursor, row: list[Any]):
fields = [column[0] for column in cursor.description]
return dict(zip(fields, row))
match self.row_factory:
case 'Row':
self.conn.row_factory = sqlite3.Row
case 'Dict':
self.conn.row_factory = dict_factory
case _:
self.conn.row_factory = None
return self.conn
except (sqlite3.Error, sqlite3.OperationalError) as e:
self.log.error(f"Error connecting to database [{type(e).__name__}] [{self.db_name}]: {e} [{call_stack()}]")
self.log.error(f"Error code: {e.sqlite_errorcode if hasattr(e, 'sqlite_errorcode') else 'N/A'}")
self.log.error(f"Error name: {e.sqlite_errorname if hasattr(e, 'sqlite_errorname') else 'N/A'}")
return None
def db_close(self):
"""close connection"""
if self.conn is not None:
self.conn.close()
self.conn = None
def db_connected(self) -> bool:
"""
Return True if db connection is not none
Returns:
bool -- _description_
"""
return True if self.conn else False
def __content_exists(self, content_name: str, sql_type: str) -> bool:
"""
Check if some content name for a certain type exists
Arguments:
content_name {str} -- _description_
sql_type {str} -- _description_
Returns:
bool -- _description_
"""
if self.conn is None:
return False
try:
cursor = self.conn.cursor()
cursor.execute("""
SELECT name
FROM sqlite_master
WHERE type = ? AND name = ?
""", (sql_type, content_name,))
return cursor.fetchone() is not None
except sqlite3.Error as e:
self.log.error(f"Error checking table [{content_name}/{sql_type}] existence: {e} [{call_stack()}]")
return False
def table_exists(self, table_name: str) -> bool:
"""
Check if a table exists in the database
"""
return self.__content_exists(table_name, 'table')
def trigger_exists(self, trigger_name: str) -> bool:
"""
Check if a triggere exits
"""
return self.__content_exists(trigger_name, 'trigger')
def index_exists(self, index_name: str) -> bool:
"""
Check if a triggere exits
"""
return self.__content_exists(index_name, 'index')
def meta_data_detail(self, table_name: str) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
"""table detail"""
query_show_table = """
SELECT
ti.cid, ti.name, ti.type, ti.'notnull', ti.dflt_value, ti.pk,
il_ii.idx_name, il_ii.idx_unique, il_ii.idx_origin, il_ii.idx_partial
FROM
sqlite_schema AS m,
pragma_table_info(m.name) AS ti
LEFT JOIN (
SELECT
il.name AS idx_name, il.'unique' AS idx_unique, il.origin AS idx_origin, il.partial AS idx_partial,
ii.cid AS tbl_cid
FROM
sqlite_schema AS m,
pragma_index_list(m.name) AS il,
pragma_index_info(il.name) AS ii
WHERE m.name = ?1
) AS il_ii ON (ti.cid = il_ii.tbl_cid)
WHERE
m.name = ?1
"""
return self.execute_query(query_show_table, (table_name,))
def execute_cursor(
self, query: str, params: tuple[Any, ...] | None = None
) -> sqlite3.Cursor | Literal[False]:
"""execute a cursor, used in execute query or return one and for fetch_row"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
cursor = self.conn.cursor()
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
return cursor
except sqlite3.Error as e:
self.log.error(f"Error during executing cursor [{query}:{params}]: {e} [{call_stack()}]")
return False
def execute_query(
self, query: str, params: tuple[Any, ...] | None = None
) -> list[tuple[Any, ...]] | list[dict[str, Any]] | Literal[False]:
"""query execute with or without params, returns result"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
if (cursor := self.execute_cursor(query, params)) is False:
return False
# fetch before commit because we need to get the RETURN before
result = cursor.fetchall()
# this is for INSERT/UPDATE/CREATE only
self.conn.commit()
return result
except sqlite3.Error as e:
self.log.error(f"Error during executing query [{query}:{params}]: {e} [{call_stack()}]")
return False
def return_one(
self, query: str, params: tuple[Any, ...] | None = None
) -> tuple[Any, ...] | dict[str, Any] | Literal[False] | None:
"""return one row, only for SELECT"""
if self.conn is None:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
if (cursor := self.execute_cursor(query, params)) is False:
return False
return cursor.fetchone()
except sqlite3.Error as e:
self.log.error(f"Error during return one: {e} [{call_stack()}]")
return False
def fetch_row(
self, cursor: sqlite3.Cursor | Literal[False]
) -> tuple[Any, ...] | dict[str, Any] | Literal[False] | None:
"""read from cursor"""
if self.conn is None or cursor is False:
self.log.warning(f"No connection [{call_stack()}]")
return False
try:
return cursor.fetchone()
except sqlite3.Error as e:
self.log.error(f"Error during fetch row: {e} [{call_stack()}]")
return False
# __END__

View File

@@ -2,15 +2,16 @@
Various debug helpers
"""
import traceback
import os
import sys
from warnings import deprecated
from typing import Tuple, Type
from types import TracebackType
from corelibs_stack_trace.stack import call_stack as call_stack_ng, exception_stack as exception_stack_ng
# _typeshed.OptExcInfo
OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType]
@deprecated("Use corelibs_stack_trace.stack.call_stack instead")
def call_stack(
start: int = 0,
skip_last: int = -1,
@@ -30,23 +31,15 @@ def call_stack(
Returns:
str -- _description_
"""
# stack = traceback.extract_stack()[start:depth]
# how many of the last entries we skip (so we do not get self), default is -1
# start cannot be negative
if skip_last > 0:
skip_last = skip_last * -1
stack = traceback.extract_stack()
__stack = stack[start:skip_last]
# start possible to high, reset start to 0
if not __stack and reset_start_if_empty:
start = 0
__stack = stack[start:skip_last]
if not separator:
separator = ' -> '
# print(f"* HERE: {dump_data(stack)}")
return f"{separator}".join(f"{os.path.basename(f.filename)}:{f.name}:{f.lineno}" for f in __stack)
return call_stack_ng(
start=start,
skip_last=skip_last,
separator=separator,
reset_start_if_empty=reset_start_if_empty
)
@deprecated("Use corelibs_stack_trace.stack.exception_stack instead")
def exception_stack(
exc_stack: OptExcInfo | None = None,
separator: str = ' -> '
@@ -61,15 +54,9 @@ def exception_stack(
Returns:
str -- _description_
"""
if exc_stack is not None:
_, _, exc_traceback = exc_stack
else:
exc_traceback = None
_, _, exc_traceback = sys.exc_info()
stack = traceback.extract_tb(exc_traceback)
if not separator:
separator = ' -> '
# print(f"* HERE: {dump_data(stack)}")
return f"{separator}".join(f"{os.path.basename(f.filename)}:{f.name}:{f.lineno}" for f in stack)
return exception_stack_ng(
exc_stack=exc_stack,
separator=separator
)
# __END__

View File

@@ -2,11 +2,13 @@
dict dump as JSON formatted
"""
import json
from warnings import deprecated
from typing import Any
from corelibs_dump_data.dump_data import dump_data as dump_data_ng
def dump_data(data: Any) -> str:
@deprecated("Use corelibs_dump_data.dump_data.dump_data instead")
def dump_data(data: Any, use_indent: bool = True) -> str:
"""
dump formated output from dict/list
@@ -16,6 +18,6 @@ def dump_data(data: Any) -> str:
Returns:
str: _description_
"""
return json.dumps(data, indent=4, ensure_ascii=False, default=str)
return dump_data_ng(data=data, use_indent=use_indent)
# __END__

View File

@@ -4,123 +4,40 @@ Profile memory usage in Python
# https://docs.python.org/3/library/tracemalloc.html
import os
import time
import tracemalloc
import linecache
from typing import Tuple
from tracemalloc import Snapshot
import psutil
from warnings import warn, deprecated
from typing import TYPE_CHECKING
from corelibs_debug.profiling import display_top as display_top_ng, display_top_str, Profiling as CoreLibsProfiling
if TYPE_CHECKING:
from tracemalloc import Snapshot
def display_top(snapshot: Snapshot, key_type: str = 'lineno', limit: int = 10) -> str:
@deprecated("Use corelibs_debug.profiling.display_top_str with data from display_top instead")
def display_top(snapshot: 'Snapshot', key_type: str = 'lineno', limit: int = 10) -> str:
"""
Print tracmalloc stats
https://docs.python.org/3/library/tracemalloc.html#pretty-top
Args:
snapshot (Snapshot): _description_
snapshot ('Snapshot'): _description_
key_type (str, optional): _description_. Defaults to 'lineno'.
limit (int, optional): _description_. Defaults to 10.
"""
snapshot = snapshot.filter_traces((
tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
tracemalloc.Filter(False, "<unknown>"),
))
top_stats = snapshot.statistics(key_type)
profiler_msg = f"Top {limit} lines"
for index, stat in enumerate(top_stats[:limit], 1):
frame = stat.traceback[0]
# replace "/path/to/module/file.py" with "module/file.py"
filename = os.sep.join(frame.filename.split(os.sep)[-2:])
profiler_msg += f"#{index}: {filename}:{frame.lineno}: {(stat.size / 1024):.1f} KiB"
line = linecache.getline(frame.filename, frame.lineno).strip()
if line:
profiler_msg += f" {line}"
other = top_stats[limit:]
if other:
size = sum(stat.size for stat in other)
profiler_msg += f"{len(other)} other: {(size / 1024):.1f} KiB"
total = sum(stat.size for stat in top_stats)
profiler_msg += f"Total allocated size: {(total / 1024):.1f} KiB"
return profiler_msg
return display_top_str(
display_top_ng(
snapshot=snapshot,
key_type=key_type,
limit=limit
)
)
class Profiling:
class Profiling(CoreLibsProfiling):
"""
Profile memory usage and elapsed time for some block
Based on: https://stackoverflow.com/a/53301648
"""
def __init__(self):
# profiling id
self.__ident: str = ''
# memory
self.__rss_before: int = 0
self.__vms_before: int = 0
# self.shared_before: int = 0
self.__rss_used: int = 0
self.__vms_used: int = 0
# self.shared_used: int = 0
# time
self.__call_start: float = 0
self.__elapsed = 0
def __get_process_memory(self) -> Tuple[int, int]:
process = psutil.Process(os.getpid())
mi = process.memory_info()
# macos does not have mi.shared
return mi.rss, mi.vms
def __elapsed_since(self) -> str:
elapsed = time.time() - self.__call_start
if elapsed < 1:
return str(round(elapsed * 1000, 2)) + "ms"
if elapsed < 60:
return str(round(elapsed, 2)) + "s"
if elapsed < 3600:
return str(round(elapsed / 60, 2)) + "min"
return str(round(elapsed / 3600, 2)) + "hrs"
def __format_bytes(self, bytes_data: int) -> str:
if abs(bytes_data) < 1000:
return str(bytes_data) + "B"
if abs(bytes_data) < 1e6:
return str(round(bytes_data / 1e3, 2)) + "kB"
if abs(bytes_data) < 1e9:
return str(round(bytes_data / 1e6, 2)) + "MB"
return str(round(bytes_data / 1e9, 2)) + "GB"
def start_profiling(self, ident: str) -> None:
"""
start the profiling
"""
self.__ident = ident
self.__rss_before, self.__vms_before = self.__get_process_memory()
self.__call_start = time.time()
def end_profiling(self) -> None:
"""
end the profiling
"""
if self.__rss_before == 0 and self.__vms_before == 0:
print("start_profile() was not called, output will be negative")
self.__elapsed = self.__elapsed_since()
__rss_after, __vms_after = self.__get_process_memory()
self.__rss_used = __rss_after - self.__rss_before
self.__vms_used = __vms_after - self.__vms_before
def print_profiling(self) -> str:
"""
print the profiling time
"""
return (
f"Profiling: {self.__ident:>20} "
f"RSS: {self.__format_bytes(self.__rss_used):>8} | "
f"VMS: {self.__format_bytes(self.__vms_used):>8} | "
f"time: {self.__elapsed:>8}"
)
warn("Use corelibs_debug.profiling.Profiling instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -5,109 +5,16 @@ Returns:
Timer: class timer for basic time run calculations
"""
from datetime import datetime, timedelta
from warnings import warn
from corelibs_debug.timer import Timer as CorelibsTimer
class Timer:
class Timer(CorelibsTimer):
"""
get difference between start and end date/time
"""
def __init__(self):
"""
init new start time and set end time to None
"""
self._overall_start_time = datetime.now()
self._overall_end_time = None
self._overall_run_time = None
self._start_time = datetime.now()
self._end_time = None
self._run_time = None
# MARK: overall run time
def overall_run_time(self) -> timedelta:
"""
overall run time difference from class launch to call of this function
Returns:
timedelta: _description_
"""
self._overall_end_time = datetime.now()
self._overall_run_time = self._overall_end_time - self._overall_start_time
return self._overall_run_time
def get_overall_start_time(self) -> datetime:
"""
get set start time
Returns:
datetime: _description_
"""
return self._overall_start_time
def get_overall_end_time(self) -> datetime | None:
"""
get set end time or None for not set
Returns:
datetime|None: _description_
"""
return self._overall_end_time
def get_overall_run_time(self) -> timedelta | None:
"""
get run time or None if run time was not called
Returns:
datetime|None: _description_
"""
return self._overall_run_time
# MARK: set run time
def run_time(self) -> timedelta:
"""
difference between start time and current time
Returns:
datetime: _description_
"""
self._end_time = datetime.now()
self._run_time = self._end_time - self._start_time
return self._run_time
def reset_run_time(self):
"""
reset start/end and run tine
"""
self._start_time = datetime.now()
self._end_time = None
self._run_time = None
def get_start_time(self) -> datetime:
"""
get set start time
Returns:
datetime: _description_
"""
return self._start_time
def get_end_time(self) -> datetime | None:
"""
get set end time or None for not set
Returns:
datetime|None: _description_
"""
return self._end_time
def get_run_time(self) -> timedelta | None:
"""
get run time or None if run time was not called
Returns:
datetime|None: _description_
"""
return self._run_time
warn("Use corelibs_debug.timer.Timer instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,12 +2,19 @@
Various small helpers for data writing
"""
from warnings import deprecated
from typing import TYPE_CHECKING
from corelibs_debug.writeline import (
write_l as write_l_ng, pr_header as pr_header_ng,
pr_title as pr_title_ng, pr_open as pr_open_ng,
pr_close as pr_close_ng, pr_act as pr_act_ng
)
if TYPE_CHECKING:
from io import TextIOWrapper
from io import TextIOWrapper, StringIO
def write_l(line: str, fpl: 'TextIOWrapper | None' = None, print_line: bool = False):
@deprecated("Use corelibs_debug.writeline.write_l instead")
def write_l(line: str, fpl: 'TextIOWrapper | StringIO | None' = None, print_line: bool = False):
"""
Write a line to screen and to output file
@@ -15,23 +22,30 @@ def write_l(line: str, fpl: 'TextIOWrapper | None' = None, print_line: bool = Fa
line (String): Line to write
fpl (Resource): file handler resource, if none write only to console
"""
if print_line is True:
print(line)
if fpl is not None:
fpl.write(line + "\n")
return write_l_ng(
line=line,
fpl=fpl,
print_line=print_line
)
# progress printers
@deprecated("Use corelibs_debug.writeline.pr_header instead")
def pr_header(tag: str, marker_string: str = '#', width: int = 35):
"""_summary_
Args:
tag (str): _description_
"""
print(f" {marker_string} {tag:^{width}} {marker_string}")
return pr_header_ng(
tag=tag,
marker_string=marker_string,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_title instead")
def pr_title(tag: str, prefix_string: str = '|', space_filler: str = '.', width: int = 35):
"""_summary_
@@ -39,9 +53,15 @@ def pr_title(tag: str, prefix_string: str = '|', space_filler: str = '.', width:
tag (str): _description_
prefix_string (str, optional): _description_. Defaults to '|'.
"""
print(f" {prefix_string} {tag:{space_filler}<{width}}:", flush=True)
return pr_title_ng(
tag=tag,
prefix_string=prefix_string,
space_filler=space_filler,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_open instead")
def pr_open(tag: str, prefix_string: str = '|', space_filler: str = '.', width: int = 35):
"""
writen progress open line with tag
@@ -50,9 +70,15 @@ def pr_open(tag: str, prefix_string: str = '|', space_filler: str = '.', width:
tag (str): _description_
prefix_string (str): prefix string. Default: '|'
"""
print(f" {prefix_string} {tag:{space_filler}<{width}} [", end="", flush=True)
return pr_open_ng(
tag=tag,
prefix_string=prefix_string,
space_filler=space_filler,
width=width
)
@deprecated("Use corelibs_debug.writeline.pr_close instead")
def pr_close(tag: str = ''):
"""
write the close tag with new line
@@ -60,9 +86,10 @@ def pr_close(tag: str = ''):
Args:
tag (str, optional): _description_. Defaults to ''.
"""
print(f"{tag}]", flush=True)
return pr_close_ng(tag=tag)
@deprecated("Use corelibs_debug.writeline.pr_act instead")
def pr_act(act: str = "."):
"""
write progress character
@@ -70,6 +97,6 @@ def pr_act(act: str = "."):
Args:
act (str, optional): _description_. Defaults to ".".
"""
print(f"{act}", end="", flush=True)
return pr_act_ng(act=act)
# __EMD__

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,22 @@
"""
simple symmetric encryption
Will be moved to CoreLibs
TODO: set key per encryption run
"""
import warnings
from corelibs_encryption.symmetric import SymmetricEncryption as CorelibsSymmetricEncryption
class SymmetricEncryption(CorelibsSymmetricEncryption):
"""
simple encryption
the encrypted package has "encrypted_data" and "salt" as fields, salt is needed to create the
key from the password to decrypt
"""
warnings.warn("Use corelibs_encryption.symmetric.SymmetricEncryption instead", DeprecationWarning, stacklevel=2)
# __END__

View File

View File

@@ -2,22 +2,39 @@
Exceptions for csv file reading and processing
"""
from warnings import warn
from corelibs_csv.csv_exceptions import (
NoCsvReader as CoreLibsNoCsvReader,
CompulsoryCsvHeaderCheckFailed as CoreLibsCompulsoryCsvHeaderCheckFailed,
CsvHeaderDataMissing as CoreLibsCsvHeaderDataMissing,
CsvRowDataKeysNotMatching as CoreLibsCsvRowDataKeysNotMatching
)
class NoCsvReader(Exception):
class NoCsvReader(CoreLibsNoCsvReader):
"""
CSV reader is none
"""
class CsvHeaderDataMissing(Exception):
class CsvHeaderDataMissing(CoreLibsCsvHeaderDataMissing):
"""
The csv reader returned None as headers, the header column in the csv file is missing
"""
class CompulsoryCsvHeaderCheckFailed(Exception):
class CompulsoryCsvHeaderCheckFailed(CoreLibsCompulsoryCsvHeaderCheckFailed):
"""
raise if the header is not matching to the excpeted values
"""
class CsvRowDataKeysNotMatching(CoreLibsCsvRowDataKeysNotMatching):
"""
raise if the row data keys do not match the expected header keys
"""
warn("Use corelibs_csv.csv_exceptions instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -0,0 +1,42 @@
"""
File check if BOM encoded, needed for CSV load
"""
from warnings import deprecated
from pathlib import Path
from corelibs_file.file_bom_encoding import (
is_bom_encoded as is_bom_encoding_ng,
get_bom_encoding_info,
BomEncodingInfo
)
@deprecated("Use corelibs_file.file_bom_encoding.is_bom_encoded instead")
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_encoding_ng(file_path)
@deprecated("Use corelibs_file.file_bom_encoding.get_bom_encoding_info instead")
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
"""
return get_bom_encoding_info(file_path)
# __END__

View File

@@ -2,10 +2,13 @@
crc handlers for file CRC
"""
import zlib
from warnings import deprecated
from pathlib import Path
from corelibs_file.file_crc import file_crc as file_crc_ng
from corelibs_file.file_handling import get_file_name
@deprecated("Use corelibs_file.file_crc.file_crc instead")
def file_crc(file_path: Path) -> str:
"""
With for loop and buffer, create file crc32
@@ -16,13 +19,10 @@ def file_crc(file_path: Path) -> str:
Returns:
str: file crc32
"""
crc = 0
with open(file_path, 'rb', 65536) as ins:
for _ in range(int((file_path.stat().st_size / 65536)) + 1):
crc = zlib.crc32(ins.read(65536), crc)
return f"{crc & 0xFFFFFFFF:08X}"
return file_crc_ng(file_path)
@deprecated("Use corelibs_file.file_handling.get_file_name instead")
def file_name_crc(file_path: Path, add_parent_folder: bool = False) -> str:
"""
either returns file name only from path
@@ -38,9 +38,6 @@ def file_name_crc(file_path: Path, add_parent_folder: bool = False) -> str:
Returns:
str: file name as string
"""
if add_parent_folder:
return str(Path(file_path.parent.name).joinpath(file_path.name))
else:
return file_path.name
return get_file_name(file_path, add_parent_folder=add_parent_folder)
# __END__

View File

@@ -2,45 +2,37 @@
File handling utilities
"""
import os
import shutil
from warnings import deprecated
from pathlib import Path
from corelibs_file.file_handling import remove_all_in_directory as remove_all_in_directory_ng
def remove_all_in_directory(directory: Path, ignore_files: list[str] | None = None, verbose: bool = False) -> bool:
@deprecated("Use corelibs_file.file_handling.remove_all_in_directory instead")
def remove_all_in_directory(
directory: Path,
ignore_files: list[str] | None = None,
verbose: bool = False,
dry_run: bool = False
) -> bool:
"""
remove all files and folders in a directory
can exclude files or folders
deprecated
Args:
directory (Path): _description_
ignore_files (list[str], optional): _description_. Defaults to None.
Arguments:
directory {Path} -- _description_
Keyword Arguments:
ignore_files {list[str] | None} -- _description_ (default: {None})
verbose {bool} -- _description_ (default: {False})
dry_run {bool} -- _description_ (default: {False})
Returns:
bool: _description_
bool -- _description_
"""
if not directory.is_dir():
return False
if ignore_files is None:
ignore_files = []
if verbose:
print(f"Remove old files in: {directory.name} [", end="", flush=True)
# remove all files and folders in given directory by recursive globbing
for file in directory.rglob("*"):
# skip if in ignore files
if file.name in ignore_files:
continue
# remove one file, or a whole directory
if file.is_file():
os.remove(file)
if verbose:
print(".", end="", flush=True)
elif file.is_dir():
shutil.rmtree(file)
if verbose:
print("/", end="", flush=True)
if verbose:
print("]", flush=True)
return True
return remove_all_in_directory_ng(
directory,
ignore_files=ignore_files,
verbose=verbose,
dry_run=dry_run
)
# __END__

View File

@@ -1,477 +0,0 @@
"""
AUTHOR: Clemens Schwaighofer
DATE CREATED: 2009/7/24 (2025/7/2)
DESCRIPTION: progress percent class (perl -> python)
HOW TO USE
* load
from progress import Progress
* init
prg = Progress()
allowed parameters to pass are (in order)
- verbose (0/1/...) : show output
- precision (-2~10) : -2 (5%), -1 (10%), 0 (normal 0-100%), 1~10 (100.m~%)
- microtime (1/0/-1) : show microtime in eta/run time
- wide time (bool) : padd time so all time column doesn't change width of line
- prefix line break (bool): add line break before string and not only after
prg = Progress(verbose = 1, precision = 2)
* settings methods
set_wide_time(bool)
set_microtime(int -1/0/1)
set_prefix_lb(bool)
set_verbose(0/1 int)
set_precision(-2~10 int)
set_linecount(int)
set_filesize(int)
set_start_time(time optional)
set_eta_start_time(time optional)
set_end_time(time optional)
show_position(file pos optional)
"""
import time
from typing import Literal
from math import floor
from corelibs.string_handling.datetime_helpers import convert_timestamp
from corelibs.string_handling.byte_helpers import format_bytes
class Progress():
"""
file progress output information
"""
def __init__(
self,
verbose: int = 0,
precision: int = 1,
microtime: Literal[-1] | Literal[1] | Literal[0] = 0,
wide_time: bool = False,
prefix_lb: bool = False
):
# set default var stuff
# max lines in input
self.linecount: int = 0
# max file size
self.filesize: int = 0
# * comma after percent
self.precision: int = 0
# * if flagged 1, then wthe wide 15 char left bound format is used
self.wide_time: bool = False
# * verbose status from outside
self.verbose: bool = False
# * microtime output for last run time (1 for enable 0 for auto -1 for disable)
self.microtime: Literal[-1] | Literal[1] | Literal[0] = 0
# micro time flag for last group
self.lg_microtime: bool = False
# = flag if output was given
self.change = 0
# = global start for the full script running time
self.start: float | None = None
# = for the eta time, can be set after a query or long read in, to not create a wrong ETA time
self.start_run: float | None = None
# loop start
self.start_time: float | None = None
# global end
self.end: float | None = None
# loop end
self.end_time: float | None = None
# run time in seconds, set when end time method is called
self.run_time: float | None = None
# = filesize current
self.count_size: int | None = None
# position current
self.count: int = 0
# last count (position)
self.current_count: int = 0
# the current file post
self.file_pos: int | None = None
# lines processed in the last run
self.lines_processed: int = 0
# time in th seconds for the last group run (until percent change)
self.last_group: float = 0
# float value, lines processed per second to the last group run
self.lines_in_last_group: float = 0
# float values, lines processed per second to complete run
self.lines_in_global: float = 0
# flaot value, bytes processes per second in the last group run
self.bytes_in_last_group: float = 0
# float value, bytes processed per second to complete run
self.bytes_in_global: float = 0
# bytes processed in last run (in bytes)
self.size_in_last_group: int = 0
# current file position 8size)
self.current_size: int = 0
# last percent position
self.last_percent: int | float = 0
# if we have normal % or in steps of 10
self.precision_ten_step: int = 0
# the default size this is precision + 4
self.percent_print: int = 5
# this is 1 if it is 1 or 0 for precision or precision size
self.percent_precision: int = 1
# prefix line with a line break
self.prefix_lb: bool = False
# estimated time to finish
self.eta: float | None = None
# run time since start
self.full_time_needed: float | None = None
# the actual output
self.string: str = ''
# initialize the class
self.set_precision(precision)
self.set_verbose(verbose)
self.set_micro_time(microtime)
self.set_wide_time(wide_time)
self.set_prefix_lb(prefix_lb)
self.set_start_time()
def reset(self):
"""
resets the current progress to 0, but keeps the overall start variables set
"""
# reset what always gets reset
self.count = 0
self.count_size = None
self.current_count = 0
self.linecount = 0
self.lines_processed = 0
self.last_group = 0
self.lines_in_last_group = 0
self.lines_in_global = 0
self.bytes_in_last_group = 0
self.bytes_in_global = 0
self.size_in_last_group = 0
self.filesize = 0
self.current_size = 0
self.last_percent = 0
self.eta = 0
self.full_time_needed = 0
self.start_run = None
self.start_time = None
self.end_time = None
def set_wide_time(self, wide_time: bool) -> bool:
"""
sets the show wide time flag
Arguments:
wide_time {bool} -- _description_
Returns:
bool -- _description_
"""
self.wide_time = wide_time
return self.wide_time
def set_micro_time(self, microtime: Literal[-1] | Literal[1] | Literal[0]) -> Literal[-1] | Literal[1] | Literal[0]:
"""sets the show microtime -1 OFF, 0 AUTO, 1 ON
Returns:
_type_ -- _description_
"""
self.microtime = microtime
return self.microtime
def set_prefix_lb(self, prefix_lb: bool) -> bool:
"""
set prefix line break flag
Arguments:
prefix_lb {bool} -- _description_
Returns:
bool -- _description_
"""
self.prefix_lb = prefix_lb
return self.prefix_lb
def set_verbose(self, verbose: int) -> bool:
"""
set the internal verbose flag to 1 if any value higher than 1 is given, else sets it to 0
Arguments:
verbose {int} -- _description_
Returns:
bool -- _description_
"""
if verbose > 0:
self.verbose = True
else:
self.verbose = False
return self.verbose
def set_precision(self, precision: int) -> int:
"""
sets the output precision size. If -2 for five step, -1 for ten step
else sets the precision normally, for 0, no precision is set, maximum precision is 10
Arguments:
precision {int} -- _description_
Returns:
int -- _description_
"""
# if not a valid number, we set it to 0
if precision < -2 or precision > 10:
precision = 0
if precision < 0:
if precision < -1:
self.precision_ten_step = 5
else:
self.precision_ten_step = 10
self.precision = 0 # no comma
self.percent_precision = 0 # no print precision
self.percent_print = 3 # max 3 length
else:
# comma values visible
self.precision = 10 if precision < 0 or precision > 10 else precision
# for calcualtion of precision
self.percent_precision = 10 if precision < 0 or precision > 10 else precision
# for the format output base is 4, plsut he percent precision length
self.percent_print = (3 if precision == 0 else 4) + self.percent_precision
# return the set precision
return self.precision
def set_linecount(self, linecount: int) -> int:
"""
set the maximum lines in this file, if value is smaller than 0 or 0, then it is set to 1
Arguments:
linecount {int} -- _description_
Returns:
int -- _description_
"""
if linecount > 0:
self.linecount = linecount
else:
self.linecount = 1
return self.linecount
def set_filesize(self, filesize: int) -> int:
"""
set the maximum filesize for this file, if value is smaller than 0 or 0, then it is set to 1
Arguments:
filesize {int} -- _description_
Returns:
int -- _description_
"""
if filesize > 0:
self.filesize = filesize
else:
self.filesize = 1
return self.filesize
def set_start_time(self, time_value: float = time.time()) -> None:
"""
initial set of the start times, auto set
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
# avoid possible double set of the original start time
if not self.start:
self.start = time_value
self.start_time = time_value
self.start_run = time_value
def set_eta_start_time(self, time_value: float = time.time()) -> None:
"""
sets the loop % run time, for correct ETA calculation
calls set start time, as the main start time is only set once
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
self.set_start_time(time_value)
def set_end_time(self, time_value: float = time.time()) -> None:
"""
set the end time
Keyword Arguments:
time_value {float} -- _description_ (default: {time.time()})
"""
self.end = time_value
self.end_time = time_value
if self.start is None:
self.start = 0
# the overall run time in micro seconds
self.run_time = self.end - self.start
def show_position(self, filepos: int = 0) -> str:
"""
processes the current position. either based on read the file size pos, or the line count
Keyword Arguments:
filepos {int} -- _description_ (default: {0})
Returns:
str -- _description_
"""
show_filesize = True # if we print from file size or line count
# microtime flags
eta_microtime = False
ftn_microtime = False
lg_microtime = False
# percent precision calc
# _p_spf = "{:." + str(self.precision) + "f}"
# output format for percent
_pr_p_spf = "{:>" + str(self.percent_print) + "." + str(self.percent_precision) + "f}"
# set the linecount precision based on the final linecount, if not, leave it empty
_pr_lc = "{}"
if self.linecount:
_pr_lc = "{:>" + str(len(str(f"{self.linecount:,}"))) + ",}"
# time format, if flag is set, the wide format is used
_pr_tf = "{}"
if self.wide_time:
_pr_tf = "{:>15}"
# count up
self.count += 1
# if we have file pos from parameter
if filepos != 0:
self.file_pos = filepos
else:
# we did not, so we set internal value
self.file_pos = self.count
# we also check if the filesize was set now
if self.filesize == 0:
self.filesize = self.linecount
# set ignore filesize output (no data)
show_filesize = False
# set the count size based on the file pos, is only used if we have filesize
self.count_size = self.file_pos
# do normal or down to 10 (0, 10, ...) %
if self.precision_ten_step:
_percent = int((self.file_pos / float(self.filesize)) * 100)
mod = _percent % self.precision_ten_step
percent = _percent if mod == 0 else self.last_percent
else:
# calc percent
percent = round(((self.file_pos / float(self.filesize)) * 100), self.precision)
# output
if percent != self.last_percent:
self.end_time = time.time() # current time (for loop time)
if self.start is None:
self.start = 0
if self.start_time is None:
self.start_time = 0
# for from the beginning
full_time_needed = self.end_time - self.start # how long from the start
self.last_group = self.end_time - self.start_time # how long for last loop
self.lines_processed = self.count - self.current_count # how many lines processed
# lines in last group
self.lines_in_last_group = (self.lines_processed / self.last_group) if self.last_group else 0
# lines in global
self.lines_in_global = (self.count / full_time_needed) if full_time_needed else 0
# if we have linecount or not
if self.linecount == 0:
full_time_per_line = (full_time_needed if full_time_needed else 1) / self.count_size # how long for all
# estimate for the rest
eta = full_time_per_line * (self.filesize - self.count_size)
else:
# how long for all
full_time_per_line = (full_time_needed if full_time_needed else 1) / self.count
# estimate for the rest
eta = full_time_per_line * (self.linecount - self.count)
# just in case ...
if eta < 0:
eta = 0
# check if to show microtime
# ON
if self.microtime == 1:
eta_microtime = ftn_microtime = lg_microtime = True
# AUTO
if self.microtime == 0:
if eta > 0 and eta < 1:
eta_microtime = True
if full_time_needed > 0 and full_time_needed < 1:
ftn_microtime = True
# pre check last group: if pre comma part is same add microtime anyway
if self.last_group > 0 and self.last_group < 1:
lg_microtime = True
if self.last_group == floor(self.last_group):
lg_microtime = True
self.last_group = floor(self.last_group)
# if with filesize or without
if show_filesize:
# last group size
self.size_in_last_group = self.count_size - self.current_size
# calc kb/s if there is any filesize data
# last group
self.bytes_in_last_group = (self.size_in_last_group / self.last_group) if self.last_group else 0
# global
self.bytes_in_global = (self.count_size / full_time_needed) if full_time_needed else 0
# only used if we run with file size for the next check
self.current_size = self.count_size
if self.verbose >= 1:
self.string = (
f"Processed {_pr_p_spf}% "
"[{} / {}] | "
f"{_pr_lc} / {_pr_lc} Lines | ETA: {_pr_tf} / TR: {_pr_tf} / "
"LR: {:,} "
"lines ({:,}) in {}, {:,.2f} ({:,.2f}) lines/s, {} ({}) b/s"
).format(
float(percent),
format_bytes(self.count_size),
format_bytes(self.filesize),
self.count,
self.linecount,
convert_timestamp(eta, eta_microtime),
convert_timestamp(full_time_needed, ftn_microtime),
self.lines_processed,
self.size_in_last_group,
convert_timestamp(self.last_group, lg_microtime),
self.lines_in_global,
self.lines_in_last_group,
format_bytes(self.bytes_in_global),
format_bytes(self.bytes_in_last_group)
)
else:
if self.verbose >= 1:
self.string = (
f"Processed {_pr_p_spf}% | {_pr_lc} / {_pr_lc} Lines "
f"| ETA: {_pr_tf} / TR: {_pr_tf} / "
"LR: {:,} lines in {}, {:,.2f} ({:,.2f}) lines/s"
).format(
float(percent),
self.count,
self.linecount,
convert_timestamp(eta, eta_microtime),
convert_timestamp(full_time_needed, ftn_microtime),
self.lines_processed,
convert_timestamp(self.last_group, lg_microtime),
self.lines_in_global,
self.lines_in_last_group
)
# prefix return string with line break if flagged
self.string = ("\n" if self.prefix_lb else '') + self.string
# print the string if verbose is turned on
if self.verbose >= 1:
print(self.string)
# write back vars
self.last_percent = percent
self.eta = eta
self.full_time_needed = full_time_needed
self.lg_microtime = lg_microtime
# for the next run, check data
self.start_time = time.time()
self.current_count = self.count
# trigger if this is a change
self.change = 1
else:
# trigger if this is a change
self.change = 0
# return string
return self.string
# } END OF ShowPosition
# __END__

View File

@@ -2,26 +2,31 @@
wrapper around search path
"""
from typing import Any, TypedDict, NotRequired
from typing import Any
from warnings import deprecated
from corelibs_search.data_search import (
ArraySearchList as CorelibsArraySearchList,
find_in_array_from_list as corelibs_find_in_array_from_list,
key_lookup as corelibs_key_lookup,
value_lookup as corelibs_value_lookup
)
class ArraySearchList(TypedDict):
class ArraySearchList(CorelibsArraySearchList):
"""find in array from list search dict"""
key: str
value: str | bool | int | float | list[str | None]
case_sensitive: NotRequired[bool]
@deprecated("Use find_in_array_from_list()")
@deprecated("Use corelibs_search.data_search.find_in_array_from_list instead")
def array_search(
search_params: list[ArraySearchList],
data: list[dict[str, Any]],
return_index: bool = False
) -> list[dict[str, Any]]:
"""depreacted, old call order"""
return find_in_array_from_list(data, search_params, return_index)
return corelibs_find_in_array_from_list(data, search_params, return_index)
@deprecated("Use corelibs_search.data_search.find_in_array_from_list instead")
def find_in_array_from_list(
data: list[dict[str, Any]],
search_params: list[ArraySearchList],
@@ -47,69 +52,14 @@ def find_in_array_from_list(
list: list of found elements, or if return index
list of dics with "index" and "data", where "data" holds the result list
"""
if not isinstance(search_params, list): # type: ignore
raise ValueError("search_params must be a list")
keys: list[str] = []
# check that key and value exist and are set
for search in search_params:
if not search.get('key') or not search.get('value'):
raise KeyError(
f"Either Key '{search.get('key', '')}' or "
f"Value '{search.get('value', '')}' is missing or empty"
)
# if double key -> abort
if search.get("key") in keys:
raise KeyError(
f"Key {search.get('key', '')} already exists in search_params"
)
keys.append(str(search['key']))
return_items: list[dict[str, Any]] = []
for si_idx, search_item in enumerate(data):
# for each search entry, all must match
matching = 0
for search in search_params:
# either Value direct or if Value is list then any of those items can match
# values are compared in lower case if case senstive is off
# lower case left side
# TODO: allow nested Keys. eg "Key: ["Key a", "key b"]" to be ["Key a"]["key b"]
if search.get("case_sensitive", True) is False:
search_value = search_item.get(str(search['key']), "").lower()
else:
search_value = search_item.get(str(search['key']), "")
# lower case right side
if isinstance(search['value'], list):
search_in = [
str(k).lower()
if search.get("case_sensitive", True) is False else k
for k in search['value']
]
elif search.get("case_sensitive", True) is False:
search_in = str(search['value']).lower()
else:
search_in = search['value']
# compare check
if (
(
isinstance(search_in, list) and
search_value in search_in
) or
search_value == search_in
):
matching += 1
if len(search_params) == matching:
if return_index is True:
# the data is now in "data sub set"
return_items.append({
"index": si_idx,
"data": search_item
})
else:
return_items.append(search_item)
# return all found or empty list
return return_items
return corelibs_find_in_array_from_list(
data,
search_params,
return_index
)
@deprecated("Use corelibs_search.data_search.key_lookup instead")
def key_lookup(haystack: dict[str, str], key: str) -> str:
"""
simple key lookup in haystack, erturns empty string if not found
@@ -121,9 +71,10 @@ def key_lookup(haystack: dict[str, str], key: str) -> str:
Returns:
str: _description_
"""
return haystack.get(key, "")
return corelibs_key_lookup(haystack, key)
@deprecated("Use corelibs_search.data_search.value_lookup instead")
def value_lookup(haystack: dict[str, str], value: str, raise_on_many: bool = False) -> str:
"""
find by value, if not found returns empty, if not raise on many returns the first one
@@ -139,11 +90,6 @@ def value_lookup(haystack: dict[str, str], value: str, raise_on_many: bool = Fal
Returns:
str: _description_
"""
keys = [__key for __key, __value in haystack.items() if __value == value]
if not keys:
return ""
if raise_on_many is True and len(keys) > 1:
raise ValueError("More than one element found with the same name")
return keys[0]
return corelibs_value_lookup(haystack, value, raise_on_many)
# __END__

View File

@@ -1,85 +1,63 @@
"""
Dict helpers
Various helper functions for type data clean up
"""
from typing import TypeAlias, Union, Dict, List, Any, cast
# definitions for the mask run below
MaskableValue: TypeAlias = Union[str, int, float, bool, None]
NestedDict: TypeAlias = Dict[str, Union[MaskableValue, List[Any], 'NestedDict']]
ProcessableValue: TypeAlias = Union[MaskableValue, List[Any], NestedDict]
from warnings import deprecated
from typing import Any
from corelibs_iterator.dict_support import (
delete_keys_from_set as corelibs_delete_keys_from_set,
convert_to_dict_type,
set_entry as corelibs_set_entry
)
def mask(
data_set: dict[str, Any],
mask_keys: list[str] | None = None,
mask_str: str = "***",
mask_str_edges: str = '_',
skip: bool = False
) -> dict[str, Any]:
@deprecated("Use corelibs_iterator.dict_support.delete_keys_from_set instead")
def delete_keys_from_set(
set_data: dict[str, Any] | list[Any] | str, keys: list[str]
) -> dict[str, Any] | list[Any] | Any:
"""
mask data for output
Checks if mask_keys list exist in any key in the data set either from the start or at the end
remove all keys from set_data
Use the mask_str_edges to define how searches inside a string should work. Default it must start
and end with '_', remove to search string in string
Arguments:
data_set {dict[str, str]} -- _description_
Keyword Arguments:
mask_keys {list[str] | None} -- _description_ (default: {None})
mask_str {str} -- _description_ (default: {"***"})
mask_str_edges {str} -- _description_ (default: {"_"})
skip {bool} -- if set to true skip (default: {False})
Args:
set_data (dict[str, Any] | list[Any] | None): _description_
keys (list[str]): _description_
Returns:
dict[str, str] -- _description_
dict[str, Any] | list[Any] | None: _description_
"""
if skip is True:
return data_set
if mask_keys is None:
mask_keys = ["encryption", "password", "secret"]
else:
# make sure it is lower case
mask_keys = [mask_key.lower() for mask_key in mask_keys]
# skip everything if there is no keys list
return corelibs_delete_keys_from_set(set_data, keys)
def should_mask_key(key: str) -> bool:
"""Check if a key should be masked"""
__key_lower = key.lower()
return any(
__key_lower.startswith(mask_key) or
__key_lower.endswith(mask_key) or
f"{mask_str_edges}{mask_key}{mask_str_edges}" in __key_lower
for mask_key in mask_keys
)
def mask_recursive(obj: ProcessableValue) -> ProcessableValue:
"""Recursively mask values in nested structures"""
if isinstance(obj, dict):
return {
key: mask_value(value) if should_mask_key(key) else mask_recursive(value)
for key, value in obj.items()
}
if isinstance(obj, list):
return [mask_recursive(item) for item in obj]
return obj
@deprecated("Use corelibs_iterator.dict_support.convert_to_dict_type instead")
def build_dict(
any_dict: Any, ignore_entries: list[str] | None = None
) -> dict[str, Any | list[Any] | dict[Any, Any]]:
"""
rewrite any AWS *TypeDef to new dict so we can add/change entrys
def mask_value(value: Any) -> Any:
"""Handle masking based on value type"""
if isinstance(value, list):
# Mask each individual value in the list
return [mask_str for _ in cast('list[Any]', value)]
if isinstance(value, dict):
# Recursively process the dictionary instead of masking the whole thing
return mask_recursive(cast('ProcessableValue', value))
# Mask primitive values
return mask_str
Args:
any_dict (Any): _description_
return {
key: mask_value(value) if should_mask_key(key) else mask_recursive(value)
for key, value in data_set.items()
}
Returns:
dict[str, Any | list[Any]]: _description_
"""
return convert_to_dict_type(any_dict, ignore_entries)
@deprecated("Use corelibs_iterator.dict_support.set_entry instead")
def set_entry(dict_set: dict[str, Any], key: str, value_set: Any) -> dict[str, Any]:
"""
set a new entry in the dict set
Arguments:
key {str} -- _description_
dict_set {dict[str, Any]} -- _description_
value_set {Any} -- _description_
Returns:
dict[str, Any] -- _description_
"""
return corelibs_set_entry(dict_set, key, value_set)
# __END__

View File

@@ -0,0 +1,52 @@
"""
Dict helpers
"""
from warnings import deprecated
from typing import TypeAlias, Union, Dict, List, Any
from corelibs_dump_data.dict_mask import (
mask as corelibs_mask
)
# definitions for the mask run below
MaskableValue: TypeAlias = Union[str, int, float, bool, None]
NestedDict: TypeAlias = Dict[str, Union[MaskableValue, List[Any], 'NestedDict']]
ProcessableValue: TypeAlias = Union[MaskableValue, List[Any], NestedDict]
@deprecated("use corelibs_dump_data.dict_mask.mask instead")
def mask(
data_set: dict[str, Any],
mask_keys: list[str] | None = None,
mask_str: str = "***",
mask_str_edges: str = '_',
skip: bool = False
) -> dict[str, Any] | list[Any]:
"""
mask data for output
Checks if mask_keys list exist in any key in the data set either from the start or at the end
Use the mask_str_edges to define how searches inside a string should work. Default it must start
and end with '_', remove to search string in string
Arguments:
data_set {dict[str, Any]} -- _description_
Keyword Arguments:
mask_keys {list[str] | None} -- _description_ (default: {None})
mask_str {str} -- _description_ (default: {"***"})
mask_str_edges {str} -- _description_ (default: {"_"})
skip {bool} -- if set to true skip (default: {False})
Returns:
dict[str, str] -- _description_
"""
return corelibs_mask(
data_set,
mask_keys,
mask_str,
mask_str_edges,
skip
)
# __END__

View File

@@ -2,13 +2,35 @@
Various dictionary, object and list hashers
"""
import json
import hashlib
from warnings import deprecated
from typing import Any
from corelibs_hash.fingerprint import (
hash_object as corelibs_hash_object,
dict_hash_frozen as corelibs_dict_hash_frozen,
dict_hash_crc as corelibs_dict_hash_crc
)
@deprecated("use corelibs_hash.fingerprint.hash_object instead")
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_
"""
return corelibs_hash_object(obj)
@deprecated("use corelibs_hash.fingerprint.hash_object instead")
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:
@@ -17,23 +39,23 @@ def dict_hash_frozen(data: dict[Any, Any]) -> int:
Returns:
str: _description_
"""
return hash(frozenset(data.items()))
return corelibs_dict_hash_frozen(data)
@deprecated("use corelibs_hash.fingerprint.dict_hash_crc and for new use hash_object instead")
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()
return corelibs_dict_hash_crc(data)
# __END__

View File

@@ -2,9 +2,16 @@
List type helpers
"""
from warnings import deprecated
from typing import Any, Sequence
from corelibs_iterator.list_support import (
convert_to_list as corelibs_convert_to_list,
is_list_in_list as corelibs_is_list_in_list,
make_unique_list_of_dicts as corelibs_make_unique_list_of_dicts
)
@deprecated("use corelibs_iterator.list_support.convert_to_list instead")
def convert_to_list(
entry: str | int | float | bool | Sequence[str | int | float | bool | Sequence[Any]]
) -> Sequence[str | int | float | bool | Sequence[Any]]:
@@ -17,11 +24,10 @@ def convert_to_list(
Returns:
list[str | int | float | bool] -- _description_
"""
if isinstance(entry, list):
return entry
return [entry]
return corelibs_convert_to_list(entry)
@deprecated("use corelibs_iterator.list_support.is_list_in_list instead")
def is_list_in_list(
list_a: Sequence[str | int | float | bool | Sequence[Any]],
list_b: Sequence[str | int | float | bool | Sequence[Any]]
@@ -37,11 +43,20 @@ def is_list_in_list(
Returns:
list[Any] -- _description_
"""
# Create sets of (value, type) tuples
set_a = set((item, type(item)) for item in list_a)
set_b = set((item, type(item)) for item in list_b)
return corelibs_is_list_in_list(list_a, list_b)
# Get the difference and extract just the values
return [item for item, _ in set_a - set_b]
@deprecated("use corelibs_iterator.list_support.make_unique_list_of_dicts instead")
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_
"""
return corelibs_make_unique_list_of_dicts(dict_list)
# __END__

View File

@@ -1,63 +0,0 @@
"""
Various helper functions for type data clean up
"""
from typing import Any, cast
def delete_keys_from_set(
set_data: dict[str, Any] | list[Any] | str, keys: list[str]
) -> dict[str, Any] | list[Any] | Any:
"""
remove all keys from set_data
Args:
set_data (dict[str, Any] | list[Any] | None): _description_
keys (list[str]): _description_
Returns:
dict[str, Any] | list[Any] | None: _description_
"""
# skip everything if there is no keys list
if not keys:
return set_data
if isinstance(set_data, dict):
for key, value in set_data.copy().items():
if key in keys:
del set_data[key]
if isinstance(value, (dict, list)):
delete_keys_from_set(value, keys) # type: ignore Partly unknown
elif isinstance(set_data, list):
for value in set_data:
if isinstance(value, (dict, list)):
delete_keys_from_set(value, keys) # type: ignore Partly unknown
else:
set_data = [set_data]
return set_data
def build_dict(
any_dict: Any, ignore_entries: list[str] | None = None
) -> dict[str, Any | list[Any] | dict[Any, Any]]:
"""
rewrite any AWS *TypeDef to new dict so we can add/change entrys
Args:
any_dict (Any): _description_
Returns:
dict[str, Any | list[Any]]: _description_
"""
if ignore_entries is None:
return cast(dict[str, Any | list[Any] | dict[Any, Any]], any_dict)
# ignore entries can be one key or key nested
# return {
# key: value for key, value in any_dict.items() if key not in ignore_entries
# }
return cast(
dict[str, Any | list[Any] | dict[Any, Any]],
delete_keys_from_set(any_dict, ignore_entries)
)
# __END__

View File

@@ -2,11 +2,12 @@
helper functions for jmespath interfaces
"""
from warnings import deprecated
from typing import Any
import jmespath
import jmespath.exceptions
from corelibs_search.jmespath_search import jmespath_search as jmespath_search_ng
@deprecated("Use corelibs_search.jmespath_search.jmespath_search instead")
def jmespath_search(search_data: dict[Any, Any] | list[Any], search_params: str) -> Any:
"""
jmespath search wrapper
@@ -22,16 +23,6 @@ def jmespath_search(search_data: dict[Any, Any] | list[Any], search_params: str)
Returns:
Any: dict/list/etc, None if nothing found
"""
try:
search_result = jmespath.search(search_params, search_data)
except jmespath.exceptions.LexerError as excp:
raise ValueError(f"Compile failed: {search_params}: {excp}") from excp
except jmespath.exceptions.ParseError as excp:
raise ValueError(f"Parse failed: {search_params}: {excp}") from excp
except TypeError as excp:
raise ValueError(f"Type error for search_params: {excp}") from excp
return search_result
# TODO: compile jmespath setup
return jmespath_search_ng(search_data, search_params)
# __END__

View File

@@ -2,30 +2,58 @@
json encoder for datetime
"""
from warnings import warn, deprecated
from typing import Any
from json import JSONEncoder
from datetime import datetime, date
from corelibs_json.json_support import (
default_isoformat as default_isoformat_ng,
DateTimeEncoder as DateTimeEncoderCoreLibs,
json_dumps as json_dumps_ng,
modify_with_jsonpath as modify_with_jsonpath_ng,
)
# subclass JSONEncoder
class DateTimeEncoder(JSONEncoder):
class DateTimeEncoder(DateTimeEncoderCoreLibs):
"""
Override the default method
cls=DateTimeEncoder
dumps(..., cls=DateTimeEncoder, ...)
"""
def default(self, o: Any) -> str | None:
if isinstance(o, (date, datetime)):
return o.isoformat()
return None
def default(obj: Any) -> str | None:
warn("Use corelibs_json.json_support.DateTimeEncoder instead", DeprecationWarning, stacklevel=2)
@deprecated("Use corelibs_json.json_support.default_isoformat instead")
def default_isoformat(obj: Any) -> str | None:
"""
default override
default=default
dumps(..., default=default, ...)
"""
if isinstance(obj, (date, datetime)):
return obj.isoformat()
return None
return default_isoformat_ng(obj)
@deprecated("Use corelibs_json.json_support.json_dumps instead")
def json_dumps(data: Any):
"""
wrapper for json.dumps with sure dump without throwing Exceptions
Arguments:
data {Any} -- _description_
Returns:
_type_ -- _description_
"""
return json_dumps_ng(data)
@deprecated("Use corelibs_json.json_support.modify_with_jsonpath instead")
def modify_with_jsonpath(data: dict[Any, Any], path: str, new_value: Any):
"""
Modify dictionary using JSONPath (more powerful than JMESPath for modifications)
"""
return modify_with_jsonpath_ng(data, path, new_value)
# __END__
# __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_stack_trace.stack import call_stack, exception_stack
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():
@@ -481,14 +563,15 @@ class Log(LogParent):
"""
Call when class is destroyed, make sure the listender is closed or else we throw a thread error
"""
if self.log_settings['add_end_info']:
if hasattr(self, 'log_settings') and self.log_settings.get('add_end_info'):
self.break_line('END')
self.stop_listener()
# 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
@@ -598,7 +833,7 @@ class Log(LogParent):
# log path, remove them stem (".log"), then add the datetime and add .log again
now = datetime.now()
# we add microseconds part to get milli seconds
new_stem=f"{log_path.stem}.{now.strftime('%Y-%m-%d_%H-%M-%S')}.{str(now.microsecond)[:3]}"
new_stem = f"{log_path.stem}.{now.strftime('%Y-%m-%d_%H-%M-%S')}.{str(now.microsecond)[:3]}"
file_handler = logging.FileHandler(
filename=log_path.with_name(f"{new_stem}{log_path.suffix}"),
encoding="utf-8",
@@ -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,38 @@
"""
Various math helpers
"""
from warnings import deprecated
import math
@deprecated("Use math.gcd instead")
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)
@deprecated("Use math.lcm instead")
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

@@ -2,9 +2,11 @@
Various HTTP auth helpers
"""
from base64 import b64encode
from warnings import deprecated
from corelibs_requests.auth_helpers import basic_auth as corelibs_basic_auth
@deprecated("use corelibs_requests.auth_helpers.basic_auth instead")
def basic_auth(username: str, password: str) -> str:
"""
setup basic auth, for debug
@@ -16,5 +18,6 @@ def basic_auth(username: str, password: str) -> str:
Returns:
str -- _description_
"""
token = b64encode(f"{username}:{password}".encode('utf-8')).decode("ascii")
return f'Basic {token}'
return corelibs_basic_auth(username, password)
# __END__

View File

@@ -3,188 +3,35 @@ requests lib interface
V2 call type
"""
from typing import Any
import warnings
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 warnings import warn
from corelibs_requests.caller import (
Caller as CoreLibsCaller,
ProxyConfig as CoreLibsProxyConfig,
ErrorResponse as CoreLibsErrorResponse
)
class Caller:
"""_summary_"""
class ErrorResponse(CoreLibsErrorResponse):
"""
Error response structure. This is returned if a request could not be completed
"""
def __init__(
self,
header: dict[str, str],
verify: bool = True,
timeout: int = 20,
proxy: dict[str, str] | None = None
):
self.headers = header
self.timeout: int = timeout
self.cafile = "/Library/Application Support/Netskope/STAgent/data/nscacert.pem"
self.verify = verify
self.proxy = proxy
def __timeout(self, timeout: int | None) -> int:
if timeout is not None:
return timeout
return self.timeout
class ProxyConfig(CoreLibsProxyConfig):
"""
Socks proxy settings
"""
def __call(
self,
action: str,
url: str,
data: dict[str, Any] | None = None,
params: dict[str, Any] | None = None,
timeout: int | None = None
) -> requests.Response | None:
"""
call wrapper, on error returns None
Args:
action (str): _description_
url (str): _description_
data (dict | None): _description_. Defaults to None.
params (dict | None): _description_. Defaults to None.
class Caller(CoreLibsCaller):
"""
requests lib interface
"""
Returns:
requests.Response | None: _description_
"""
if data is None:
data = {}
try:
response = None
if action == "get":
response = requests.get(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "post":
response = requests.post(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "put":
response = requests.put(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "patch":
response = requests.patch(
url,
params=params,
json=data,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
elif action == "delete":
response = requests.delete(
url,
params=params,
headers=self.headers,
timeout=self.__timeout(timeout),
verify=self.verify,
proxies=self.proxy
)
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
def get(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
"""
get data
Args:
url (str): _description_
params (dict | None): _description_
Returns:
requests.Response: _description_
"""
return self.__call('get', url, params=params)
def post(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""
post data
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('post', url, data, params)
def put(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""_summary_
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('put', url, data, params)
def patch(
self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None
) -> requests.Response | None:
"""_summary_
Args:
url (str): _description_
data (dict | None): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('patch', url, data, params)
def delete(self, url: str, params: dict[str, Any] | None = None) -> requests.Response | None:
"""
delete
Args:
url (str): _description_
params (dict | None): _description_
Returns:
requests.Response | None: _description_
"""
return self.__call('delete', url, params=params)
warn(
"corelibs.requests_handling.caller is deprecated, use corelibs_requests.caller instead",
DeprecationWarning, stacklevel=2
)
# __END__

View File

@@ -0,0 +1,44 @@
"""
AUTHOR: Clemens Schwaighofer
DATE CREATED: 2009/7/24 (2025/7/2)
DESCRIPTION: progress percent class (perl -> python)
HOW TO USE
* load
from progress import Progress
* init
prg = Progress()
allowed parameters to pass are (in order)
- verbose (0/1/...) : show output
- precision (-2~10) : -2 (5%), -1 (10%), 0 (normal 0-100%), 1~10 (100.m~%)
- microtime (1/0/-1) : show microtime in eta/run time
- wide time (bool) : padd time so all time column doesn't change width of line
- prefix line break (bool): add line break before string and not only after
prg = Progress(verbose = 1, precision = 2)
* settings methods
set_wide_time(bool)
set_microtime(int -1/0/1)
set_prefix_lb(bool)
set_verbose(0/1 int)
set_precision(-2~10 int)
set_linecount(int)
set_filesize(int)
set_start_time(time optional)
set_eta_start_time(time optional)
set_end_time(time optional)
show_position(file pos optional)
"""
from warnings import warn
from corelibs_progress.progress import Progress as CoreProgress # for type checking only
class Progress(CoreProgress):
"""
file progress output information
"""
warn("Use 'corelibs_progress.progress.Progress'", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,13 +2,16 @@
Helper methods for scripts
"""
import time
import os
import sys
from warnings import deprecated
from pathlib import Path
import psutil
from corelibs_script.script_support import (
wait_abort as corelibs_wait_abort,
lock_run as corelibs_lock_run,
unlock_run as corelibs_unlock_run,
)
@deprecated("use corelibs_script.script_support.wait_abort instead")
def wait_abort(sleep: int = 5) -> None:
"""
wait a certain time for an abort command
@@ -16,18 +19,10 @@ def wait_abort(sleep: int = 5) -> None:
Keyword Arguments:
sleep {int} -- _description_ (default: {5})
"""
try:
print(f"Waiting {sleep} seconds (Press CTRL +C to abort) [", end="", flush=True)
for _ in range(1, sleep):
print(".", end="", flush=True)
time.sleep(1)
print("]", flush=True)
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
print("\n\n")
corelibs_wait_abort(sleep)
@deprecated("use corelibs_script.script_support.lock_run instead")
def lock_run(lock_file: Path) -> None:
"""
lock a script run
@@ -41,44 +36,10 @@ def lock_run(lock_file: Path) -> None:
Exception: _description_
IOError: _description_
"""
no_file = False
run_pid = os.getpid()
# or os.path.isfile()
try:
with open(lock_file, "r", encoding="UTF-8") as fp:
exists = False
pid = fp.read()
fp.close()
if pid:
# check if this pid exists
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
if pid == proc.info['pid']:
exists = True
break
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
# in case we cannot access
continue
if not exists:
# no pid but lock file, unlink
try:
lock_file.unlink()
no_file = True
except IOError as e:
raise IOError(f"Cannot remove lock_file: {lock_file}: {e}") from e
else:
raise IOError(f"Script is already running with PID {pid}")
except IOError:
no_file = True
if no_file:
try:
with open(lock_file, "w", encoding="UTF-8") as fp:
fp.write(str(run_pid))
fp.close()
except IOError as e:
raise IOError(f"Cannot open run lock file '{lock_file}' for writing: {e}") from e
corelibs_lock_run(lock_file)
@deprecated("use corelibs_script.script_support.unlock_run instead")
def unlock_run(lock_file: Path) -> None:
"""
removes the lock file
@@ -89,9 +50,6 @@ def unlock_run(lock_file: Path) -> None:
Raises:
Exception: _description_
"""
try:
lock_file.unlink()
except IOError as e:
raise IOError(f"Cannot remove lock_file: {lock_file}: {e}") from e
corelibs_unlock_run(lock_file)
# __END__

View File

@@ -2,7 +2,11 @@
Format bytes
"""
from warnings import deprecated
from corelibs_strings.string_format import format_bytes as corelibs_format_bytes
@deprecated("Use corelibs_strings.string_format.format_bytes instead")
def format_bytes(byte_value: float | int | str) -> str:
"""
Format a byte value to a human readable string
@@ -14,24 +18,9 @@ def format_bytes(byte_value: float | int | str) -> str:
str -- _description_
"""
# if string exit
if isinstance(byte_value, str):
return byte_value
# empty byte value is set to 0
if not byte_value:
byte_value = float(0)
# if not float, convert to flaot
if isinstance(byte_value, int):
byte_value = float(byte_value)
# loop through valid extensions
for unit in ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"]:
# never go into the negativ and check if it is smaller than next set
# if it is, print out return string
if abs(byte_value) < 1024.0:
return f"{byte_value:,.2f} {unit}"
# divided for the next loop check
byte_value /= 1024.0
# if it is too big, return YB
return f"{byte_value:,.2f} YB"
return corelibs_format_bytes(
byte_value=byte_value,
)
# __NED__

View File

@@ -1,63 +0,0 @@
"""
Various string based date/time helpers
"""
from math import floor
import time
def convert_timestamp(timestamp: float | int, show_micro: bool = True) -> str:
"""
format timestamp into human readable format
Arguments:
timestamp {float} -- _description_
Keyword Arguments:
show_micro {bool} -- _description_ (default: {True})
Returns:
str -- _description_
"""
# 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])
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_micro:
time_string += f" {ms}ms" if ms else " 0ms"
return time_string
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
Arguments:
timestamp {float} -- _description_
Keyword Arguments:
timestamp_format {_type_} -- _description_ (default: {"%Y-%m-%d %H:%M:%S"})
Returns:
str -- _description_
"""
return time.strftime(timestamp_format, time.localtime(timestamp))
# __END__

View File

@@ -2,225 +2,18 @@
Format double byte strings to exact length
"""
import unicodedata
from warnings import warn
from corelibs_double_byte_format.double_byte_string_format import (
DoubleByteFormatString as CorelibsDoubleByteFormatString
)
class DoubleByteFormatString:
class DoubleByteFormatString(CorelibsDoubleByteFormatString):
"""
Format a string to exact length
"""
def __init__(
self,
string: str,
cut_length: int,
format_length: int | None = None,
placeholder: str = '..',
format_string: str = '{{:<{len}}}'
):
"""
shorts a string to exact cut length and sets it to format length
after "cut_length" cut the "placeholder" will be added, so that the new cut_length is never
larget than the cut_length given (".." is counted to cut_length)
if format_length if set and outside format_length will be set
the cut_length is adjusted to format_length if the format_length is shorter
Example
"Foo bar baz" 10 charcters -> 5 cut_length -> 10 format_length
"Foo.. "
use class.get_string_short() for cut length shortend string
use class.get_string_short_formated() to get the shorted string to format length padding
creates a class that shortens and sets the format length
to use with a print format run the format needs to be pre set in
the style of {{:<{len}}} style
self.get_string_short_formated() for the "len" parameter
Args:
string (str): string to work with
cut_length (int): width to shorten to
format_length (int | None): format length. Defaults to None
placeholder (str, optional): placeholder to put after shortened string. Defaults to '..'.
format_string (str, optional): format string. Defaults to '{{:<{len}}}'
"""
# output variables
self.string_short: str = ''
self.string_width_value: int = 0
self.string_short_width: int = 0
self.format_length_value: int = 0
# internal varaibles
self.placeholder: str = placeholder
# original string
self.string: str = ''
# width to cut string to
self.cut_length: int = 0
# format length to set to
self.format_length: int = 0
# main string
self.string = str(string)
self.format_string: str = format_string
# if width is > 0 set, else set width of string (fallback)
if cut_length > 0:
self.cut_length = cut_length
elif cut_length <= 0:
self.cut_length = self.__string_width_calc(self.string)
# format length set, if not set or smaller than 0, set to width of string
self.format_length = self.cut_length
if format_length is not None and format_length > 0:
self.format_length = format_length
# check that width is not larger then length if yes, set width to length
self.cut_length = min(self.cut_length, self.format_length)
# process the string shorten and format length calculation
self.process()
def process(self):
"""
runs all the class methods to set string length, the string shortened
and the format length
"""
# call the internal ones to set the data
if self.string:
self.__string_width()
self.__shorten_string()
if self.format_length:
self.__format_length()
def get_string_short(self) -> str:
"""
get the shortend string
Returns:
str -- _description_
"""
return self.string_short
def get_string_short_formated(self, format_string: str = '{{:<{len}}}') -> str:
"""
get the formatted string
Keyword Arguments:
format_string {_type_} -- _description_ (default: {'{{:<{len}}}'})
Returns:
str -- _description_
"""
if not format_string:
format_string = self.format_string
return format_string.format(
len=self.get_format_length()
).format(
self.get_string_short()
)
def get_format_length(self) -> int:
"""
get the format length for outside length set
Returns:
int -- _description_
"""
return self.format_length_value
def get_cut_length(self) -> int:
"""
get the actual cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_cut_length(self) -> int:
"""
get the requested cut length
Returns:
int -- _description_
"""
return self.cut_length
def get_requested_format_length(self) -> int:
"""
get the requested format length
Returns:
int -- _description_
"""
return self.format_length
def __string_width_calc(self, string: str) -> int:
"""
does the actual string width calculation
Args:
string (str): string to calculate from
Returns:
int: stringth width
"""
return sum(1 + (unicodedata.east_asian_width(c) in "WF") for c in string)
def __string_width(self):
"""
calculates the string width based on the characters
this is an internal method and should not be called on itself
"""
# only run if string is set and is valid string
if self.string:
# calculate width. add +1 for each double byte character
self.string_width_value = self.__string_width_calc(self.string)
def __format_length(self):
"""
set the format length based on the length for the format
and the shortend string
this is an internal method and should not be called on itself
"""
if not self.string_short:
self.__shorten_string()
# get correct format length based on string
if (
self.string_short and
self.format_length > 0 and
self.string_short_width > 0
):
# length: format length wanted
# substract the width of the shortend string - the length of the shortend string
self.format_length_value = self.format_length - (self.string_short_width - len(self.string_short))
else:
# if we have nothing to shorten the length, keep the old one
self.format_length_value = self.format_length
def __shorten_string(self):
"""
shorten string down to set width
this is an internal method and should not be called on itself
"""
# set string width if not set
if not self.string_width_value:
self.__string_width()
# if the double byte string width is larger than the wanted width
if self.string_width_value > self.cut_length:
cur_len = 0
self.string_short = ''
for char in str(self.string):
# set the current length if we add the character
cur_len += 2 if unicodedata.east_asian_width(char) in "WF" else 1
# if the new length is smaller than the output length to shorten too add the char
if cur_len <= (self.cut_length - len(self.placeholder)):
self.string_short += char
self.string_short_width = cur_len
# return string with new width and placeholder
self.string_short = f"{self.string_short}{self.placeholder}"
self.string_short_width += len(self.placeholder)
else:
# if string is same saze just copy
self.string_short = self.string
warn("Use 'corelibs_double_byte_format.double_byte_string_format' instead", DeprecationWarning, stacklevel=2)
# __END__

View File

@@ -2,10 +2,11 @@
Various hash helpers for strings and things
"""
import re
import hashlib
from warnings import deprecated
from corelibs_hash.string_hash import crc32b_fix as corelibs_crc32b_fix, sha1_short as corelibs_sha1_short
@deprecated("Use corelibs_hash.string_hash.crc32b_fix instead")
def crc32b_fix(crc: str) -> str:
"""
fix a CRC32B with wrong order (from old PHP)
@@ -16,15 +17,10 @@ def crc32b_fix(crc: str) -> str:
Returns:
str -- _description_
"""
# left pad with 0 to 8 chars
crc = ("0" * (8 - len(crc))) + crc
# flip two chars (byte hex)
crc = re.sub(
r"^([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})([a-z0-9]{2})$", r"\4\3\2\1", crc
)
return crc
return corelibs_crc32b_fix(crc)
@deprecated("Use corelibs_hash.string_hash.sha1_short instead")
def sha1_short(string: str) -> str:
"""
Return a 9 character long SHA1 part
@@ -35,6 +31,6 @@ def sha1_short(string: str) -> str:
Returns:
str -- _description_
"""
return hashlib.sha1(string.encode('utf-8')).hexdigest()[:9]
return corelibs_sha1_short(string)
# __END__

View File

@@ -2,10 +2,16 @@
String helpers
"""
from decimal import Decimal, getcontext
from textwrap import shorten
from warnings import deprecated
from corelibs_strings.string_support import (
shorten_string as corelibs_shorten_string,
left_fill as corelibs_left_fill,
prepare_url_slash as corelibs_prepare_url_slash,
)
from corelibs_strings.string_format import format_number as corelibs_format_number
@deprecated("Use corelibs_strings.string_support.shorten_string instead")
def shorten_string(
string: str | int | float, length: int, hard_shorten: bool = False, placeholder: str = " [~]"
) -> str:
@@ -22,25 +28,15 @@ def shorten_string(
Returns:
str: _description_
"""
string = str(string)
# if placeholder > lenght
if len(string) > length:
if hard_shorten is True or " " not in string:
# hard shorten error
if len(placeholder) > length:
raise ValueError(f"Cannot shorten string: placeholder {placeholder} is too large for max width")
short_string = f"{string[:(length - len(placeholder))]}{placeholder}"
else:
try:
short_string = shorten(string, width=length, placeholder=placeholder)
except ValueError as e:
raise ValueError(f"Cannot shorten string: {e}") from e
else:
short_string = string
return short_string
return corelibs_shorten_string(
string=string,
length=length,
hard_shorten=hard_shorten,
placeholder=placeholder,
)
@deprecated("Use corelibs_strings.string_support.left_fill instead")
def left_fill(string: str, width: int, char: str = " ") -> str:
"""
left fill for a certain length to fill a max size
@@ -57,19 +53,14 @@ def left_fill(string: str, width: int, char: str = " ") -> str:
Returns:
str -- _description_
"""
# the width needs to be string
if width < 0:
width = len(string)
# char can only be one length long
if len(char) != 1:
char = " "
return (
"{:"
f"{char}>{width}"
"}"
).format(string)
return corelibs_left_fill(
string=string,
width=width,
char=char,
)
@deprecated("Use corelibs_strings.string_format.format_number instead")
def format_number(number: float, precision: int = 0) -> str:
"""
format numbers, current trailing zeros does not work
@@ -87,18 +78,26 @@ def format_number(number: float, precision: int = 0) -> str:
Returns:
str -- _description_
"""
if precision < 0 or precision > 100:
precision = 0
if precision > 0:
getcontext().prec = precision
# make it a string to avoid mangling
_number = Decimal(str(number))
else:
_number = number
return (
"{:,."
f"{str(precision)}"
"f}"
).format(_number)
return corelibs_format_number(
number=number,
precision=precision,
)
@deprecated("Use corelibs_strings.string_support.prepare_url_slash instead")
def prepare_url_slash(url: str) -> str:
"""
if the URL does not start with /, add slash
strip all double slashes in URL
Arguments:
url {str} -- _description_
Returns:
str -- _description_
"""
return corelibs_prepare_url_slash(
url=url,
)
# __END__

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

@@ -1,112 +0,0 @@
"""
Current timestamp strings and time zones
"""
import re
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from corelibs.var_handling.var_helpers import is_float
class TimeParseError(Exception):
"""Custom exception for time parsing errors."""
class TimeUnitError(Exception):
"""Custom exception for time parsing errors."""
class TimestampStrings:
"""
set default time stamps
"""
TIME_ZONE: str = 'Asia/Tokyo'
def __init__(self, time_zone: str | None = None):
self.timestamp_now = datetime.now()
self.time_zone = time_zone if time_zone is not None else self.TIME_ZONE
try:
self.timestamp_now_tz = datetime.now(ZoneInfo(self.time_zone))
except ZoneInfoNotFoundError as e:
raise ValueError(f'Zone could not be loaded [{self.time_zone}]: {e}') from e
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")
def convert_to_seconds(time_string: str | int | float) -> int:
"""
Conver a string with time units into a seconds string
The following units are allowed
Y: 365 days
M: 30 days
d, h, m, s
Arguments:
time_string {str} -- _description_
Raises:
ValueError: _description_
Returns:
int -- _description_
"""
# 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)
# 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

View File

@@ -0,0 +1,25 @@
"""
Enum base classes
"""
import warnings
from corelibs_enum_base.enum_base import EnumBase as CorelibsEnumBase
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
"""
# At the module level, issue a deprecation warning
warnings.warn("Use corelibs_enum_base.enum_base.EnumBase instead", DeprecationWarning, stacklevel=2)
# __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
@@ -24,6 +31,7 @@ match_source_list=foo,bar
element_a=Static energy
element_b=123.5
element_c=True
elemend_d=AB:CD;EF
email=foo@bar.com,other+bar-fee@domain-com.cp,
email_not_mandatory=
email_bad=gii@bar.com
@@ -31,3 +39,6 @@ email_bad=gii@bar.com
[LoadTest]
a.b.c=foo
d:e:f=bar
[ErrorTest]
some_value=42

View File

@@ -4,7 +4,7 @@ Settings loader test
import re
from pathlib import Path
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.logging_handling.log import Log
from corelibs.config_handling.settings_loader import SettingsLoader
from corelibs.config_handling.settings_loader_handling.settings_loader_check import SettingsLoaderCheck
@@ -12,6 +12,7 @@ from corelibs.config_handling.settings_loader_handling.settings_loader_check imp
SCRIPT_PATH: Path = Path(__file__).resolve().parent
ROOT_PATH: Path = SCRIPT_PATH
CONFIG_DIR: Path = Path("config")
LOG_DIR: Path = Path("log")
CONFIG_FILE: str = "settings.ini"
@@ -20,15 +21,9 @@ 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
script_path: Path = Path(__file__).resolve().parent
log = Log(
log_path=script_path.joinpath('log', 'settings_loader.log'),
log_path=ROOT_PATH.joinpath(LOG_DIR, 'settings_loader.log'),
log_name="Settings Loader",
log_settings={
"log_level_console": 'DEBUG',
@@ -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,236 @@
#!/usr/bin/env python3
"""
date string helper test
"""
from datetime import datetime
from corelibs.datetime_handling.datetime_helpers import (
get_datetime_iso8601, get_system_timezone, parse_timezone_data, 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
)
def __get_datetime_iso8601():
"""
Comment
"""
for tz in [
'', 'Asia/Tokyo', 'UTC', 'Europe/Vienna',
'America/New_York', 'Australia/Sydney',
'invalid'
]:
print(f"{tz} -> {get_datetime_iso8601(tz)}")
def __parse_timezone_data():
for tz in [
'JST', 'KST', 'UTC', 'CET', 'CEST',
]:
print(f"{tz} -> {parse_timezone_data(tz)}")
def __validate_date():
"""
Comment
"""
test_dates = [
"2024-01-01",
"2024-02-29", # Leap year
"2023-02-29", # Invalid date
"2024-13-01", # Invalid month
"2024-00-10", # Invalid month
"2024-04-31", # Invalid day
"invalid-date"
]
for date_str in test_dates:
is_valid = validate_date(date_str)
print(f"Date '{date_str}' is valid: {is_valid}")
# also test not before and not after
not_before_dates = [
"2023-12-31",
"2024-01-01",
"2024-02-29",
]
not_after_dates = [
"2024-12-31",
"2024-11-30",
"2025-01-01",
]
for date_str in not_before_dates:
datetime.strptime(date_str, "%Y-%m-%d") # Ensure valid date format
is_valid = validate_date(date_str, not_before=datetime.strptime("2024-01-01", "%Y-%m-%d"))
print(f"Date '{date_str}' is valid (not before 2024-01-01): {is_valid}")
for date_str in not_after_dates:
is_valid = validate_date(date_str, not_after=datetime.strptime("2024-12-31", "%Y-%m-%d"))
print(f"Date '{date_str}' is valid (not after 2024-12-31): {is_valid}")
for date_str in test_dates:
is_valid = validate_date(
date_str,
not_before=datetime.strptime("2024-01-01", "%Y-%m-%d"),
not_after=datetime.strptime("2024-12-31", "%Y-%m-%d")
)
print(f"Date '{date_str}' is valid (2024 only): {is_valid}")
def __parse_flexible_date():
for date_str in [
"2024-01-01",
"01/02/2024",
"February 29, 2024",
"Invalid date",
"2025-01-01 12:18:10",
"2025-01-01 12:18:10.566",
"2025-01-01T12:18:10.566",
"2025-01-01T12:18:10.566+02:00",
]:
print(f"{date_str} -> {parse_flexible_date(date_str)}")
def __compare_dates():
for date1, date2 in [
("2024-01-01 12:00:00", "2024-01-01 15:30:00"),
("2024-01-02", "2024-01-01"),
("2024-01-01T10:00:00+02:00", "2024-01-01T08:00:00Z"),
("invalid-date", "2024-01-01"),
("2024-01-01", "invalid-date"),
("invalid-date", "also-invalid"),
]:
result = compare_dates(date1, date2)
print(f"Comparing '{date1}' and '{date2}': {result}")
def __find_newest_datetime_in_list():
date_list = [
"2024-01-01 12:00:00",
"2024-01-02 09:30:00",
"2023-12-31 23:59:59",
"2024-01-02 15:45:00",
"2024-01-02T15:45:00.001",
"invalid-date",
]
newest_date = find_newest_datetime_in_list(date_list)
print(f"Newest date in list: {newest_date}")
def __parse_day_of_week_range():
ranges = [
"Mon-Fri",
"Saturday-Sunday",
"Wed-Mon",
"Fri-Fri",
"mon-tue",
"Invalid-Range"
]
for range_str in ranges:
try:
days = parse_day_of_week_range(range_str)
print(f"Day range '{range_str}' -> {days}")
except ValueError as e:
print(f"[!] Error parsing day range '{range_str}': {e}")
def __parse_time_range():
ranges = [
"08:00-17:00",
"22:00-06:00",
"12:30-12:30",
"invalid-range"
]
for range_str in ranges:
try:
start_time, end_time = parse_time_range(range_str)
print(f"Time range '{range_str}' -> Start: {start_time}, End: {end_time}")
except ValueError as e:
print(f"[!] Error parsing time range '{range_str}': {e}")
def __times_overlap_or_connect():
time_format = "%H:%M"
time_ranges = [
(("08:00", "12:00"), ("11:00", "15:00")), # Overlap
(("22:00", "02:00"), ("01:00", "05:00")), # Overlap across midnight
(("10:00", "12:00"), ("12:00", "14:00")), # Connect
(("09:00", "11:00"), ("12:00", "14:00")), # No overlap
]
for (start1, end1), (start2, end2) in time_ranges:
start1 = datetime.strptime(start1, time_format).time()
end1 = datetime.strptime(end1, time_format).time()
start2 = datetime.strptime(start2, time_format).time()
end2 = datetime.strptime(end2, time_format).time()
overlap = times_overlap_or_connect((start1, end1), (start2, end2))
overlap_connect = times_overlap_or_connect((start1, end1), (start2, end2), True)
print(f"Time ranges {start1}-{end1} and {start2}-{end2} overlap/connect: {overlap}/{overlap_connect}")
def __is_time_in_range():
time_format = "%H:%M:%S"
test_cases = [
("10:00:00", "09:00:00", "11:00:00"),
("23:30:00", "22:00:00", "01:00:00"), # Across midnight
("05:00:00", "06:00:00", "10:00:00"), # Not in range
("12:00:00", "12:00:00", "12:00:00"), # Exact match
]
for (check_time, start_time, end_time) in test_cases:
start_time = datetime.strptime(start_time, time_format).time()
end_time = datetime.strptime(end_time, time_format).time()
in_range = is_time_in_range(
f"{check_time}", start_time.strftime("%H:%M:%S"), end_time.strftime("%H:%M:%S")
)
print(f"Time {check_time} in range {start_time}-{end_time}: {in_range}")
def __reorder_weekdays_from_today():
for base_day in [
"Tue", "Wed", "Sunday", "Fri", "InvalidDay"
]:
try:
reordered_days = reorder_weekdays_from_today(base_day)
print(f"Reordered weekdays from {base_day}: {reordered_days}")
except ValueError as e:
print(f"[!] Error reordering weekdays from '{base_day}': {e}")
def main() -> None:
"""
Comment
"""
print("\nDatetime ISO 8601 tests:\n")
__get_datetime_iso8601()
print("\nSystem time test:")
print(f"System time: {get_system_timezone()}")
print("\nParse timezone data tests:\n")
__parse_timezone_data()
print("\nValidate date tests:\n")
__validate_date()
print("\nParse flexible date tests:\n")
__parse_flexible_date()
print("\nCompare dates tests:\n")
__compare_dates()
print("\nFind newest datetime in list tests:\n")
__find_newest_datetime_in_list()
print("\nParse day of week range tests:\n")
__parse_day_of_week_range()
print("\nParse time range tests:\n")
__parse_time_range()
print("\nTimes overlap or connect tests:\n")
__times_overlap_or_connect()
print("\nIs time in range tests:\n")
__is_time_in_range()
print("\nReorder weekdays from today tests:\n")
__reorder_weekdays_from_today()
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
timestamp string checks
"""
from corelibs.datetime_handling.timestamp_convert import (
convert_timestamp, seconds_to_string, convert_to_seconds, TimeParseError, TimeUnitError
)
def main() -> None:
"""
Comment
"""
print("\n--- Testing convert_to_seconds ---\n")
test_cases = [
"5M 6d", # 5 months, 6 days
"2h 30m 45s", # 2 hours, 30 minutes, 45 seconds
"1Y 2M 3d", # 1 year, 2 months, 3 days
"1h", # 1 hour
"30m", # 30 minutes
"2 hours 15 minutes", # 2 hours, 15 minutes
"1d 12h", # 1 day, 12 hours
"3M 2d 4h", # 3 months, 2 days, 4 hours
"45s", # 45 seconds
"-45s", # -45 seconds
"-1h", # -1 hour
"-30m", # -30 minutes
"-2h 30m 45s", # -2 hours, 30 minutes, 45 seconds
"-1d 12h", # -1 day, 12 hours
"-3M 2d 4h", # -3 months, 2 days, 4 hours
"-1Y 2M 3d", # -1 year, 2 months, 3 days
"-2 hours 15 minutes", # -2 hours, 15 minutes
"-1 year 2 months", # -1 year, 2 months
"-2Y 6M 15d 8h 30m 45s", # Complex negative example
"1 year 2 months", # 1 year, 2 months
"2Y 6M 15d 8h 30m 45s", # Complex example
# invalid tests
"5M 6d 2M", # months appears twice
"2h 30m 45s 1h", # hours appears twice
"1d 2 days", # days appears twice (short and long form)
"30m 45 minutes", # minutes appears twice
"1Y 2 years", # years appears twice
"1x 2 yrs", # invalid names
123, # int
789.12, # float
456.56, # float, high
"4566", # int as string
"5551.12", # float as string
"5551.56", # float, high as string
]
for time_string in test_cases:
try:
result = convert_to_seconds(time_string)
print(f"Human readable to seconds: {time_string} => {result}")
except (TimeParseError, TimeUnitError) as e:
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
print("\n--- Testing seconds_to_string and convert_timestamp ---\n")
test_values = [
'as is string',
-172800.001234, # -2 days, -0.001234 seconds
-90061.789, # -1 day, -1 hour, -1 minute, -1.789 seconds
-3661.456, # -1 hour, -1 minute, -1.456 seconds
-65.123, # -1 minute, -5.123 seconds
-1.5, # -1.5 seconds
-0.001, # -1 millisecond
-0.000001, # -1 microsecond
0, # 0 seconds
0.000001, # 1 microsecond
0.001, # 1 millisecond
1.5, # 1.5 seconds
65.123, # 1 minute, 5.123 seconds
3661.456, # 1 hour, 1 minute, 1.456 seconds
90061.789, # 1 day, 1 hour, 1 minute, 1.789 seconds
172800.001234 # 2 days, 0.001234 seconds
]
for time_value in test_values:
result = seconds_to_string(time_value, show_microseconds=True)
result_alt = convert_timestamp(time_value, show_microseconds=True)
print(f"Seconds to human readable: {time_value} => {result} / {result_alt}")
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
test-run/db_handling/log/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,139 @@
"""
SQL Main wrapper test
"""
from pathlib import Path
from uuid import uuid4
import json
from corelibs_dump_data.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

@@ -0,0 +1,146 @@
"""
SQLite IO test
"""
from pathlib import Path
from uuid import uuid4
import json
import sqlite3
from corelibs_dump_data.dump_data import dump_data
from corelibs.logging_handling.log import Log, Logger
from corelibs.db_handling.sqlite_io import SQLiteIO
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_io.log'),
log_name="SQLite IO",
log_settings={
"log_level_console": 'DEBUG',
"log_level_file": 'DEBUG',
}
)
db = SQLiteIO(
log=Logger(log.get_logger_settings()),
db_name=ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_io.db'),
row_factory='Dict'
)
if db.db_connected():
log.info(f"Connected to DB: {db.db_name}")
if db.trigger_exists('trg_test_a_set_date_updated_on_update'):
log.info("Trigger trg_test_a_set_date_updated_on_update exists")
if db.table_exists('test_a'):
log.info("Table test_a exists, dropping for clean test")
db.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 = db.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 = db.execute_query(trigger_sql)
log.debug(f"Create trigger result: {result}")
result = db.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 = db.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 = db.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 = db.execute_query("SELECT * FROM test_a;")
log.debug(f"Select data result: {dump_data(result)}")
result = db.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 = db.execute_query(
sql,
(
'Some updated text A',
__uid,
)
)
log.debug(f"Update data result: {dump_data(result)}")
result = db.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
log.debug(f"Fetch row after update result: {dump_data(result)}")
db.db_close()
db = SQLiteIO(
log=Logger(log.get_logger_settings()),
db_name=ROOT_PATH.joinpath(DATABASE_DIR, 'test_sqlite_io.db'),
row_factory='Row'
)
result = db.return_one("SELECT * FROM test_a WHERE uid = ?;", (__uid,))
if result is not None and result is not False:
log.debug(f"Fetch row result: {dump_data(result)} -> {dict(result)} -> {result.keys()}")
log.debug(f"Access via index: {result[5]} -> {result['text_a']}")
if isinstance(result, sqlite3.Row):
log.debug('Result is sqlite3.Row as expected')
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""
Symmetric encryption test
"""
import json
from corelibs_dump_data.dump_data import dump_data
from corelibs.encryption_handling.symmetric_encryption import SymmetricEncryption
def main() -> None:
"""
Comment
"""
password = "strongpassword"
se = SymmetricEncryption(password)
plaintext = "Hello, World!"
ciphertext = se.encrypt_with_metadata_return_str(plaintext)
decrypted = se.decrypt_with_metadata(ciphertext)
print(f"Encrypted: {dump_data(json.loads(ciphertext))}")
print(f"Input: {plaintext} -> {decrypted}")
static_ciphertext = SymmetricEncryption.encrypt_data(plaintext, password)
decrypted = SymmetricEncryption.decrypt_data(static_ciphertext, password)
print(f"Static Encrypted: {dump_data(json.loads(static_ciphertext))}")
print(f"Input: {plaintext} -> {decrypted}")
if __name__ == "__main__":
main()
# __END__

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""
BOM check for files
"""
from pathlib import Path
from corelibs_dump_data.dump_data import dump_data
from corelibs.file_handling.file_bom_encoding import is_bom_encoded, is_bom_encoded_info
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

@@ -5,7 +5,7 @@ Search data tests
iterator_handling.data_search
"""
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.iterator_handling.data_search import find_in_array_from_list, ArraySearchList
@@ -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,8 +2,10 @@
Iterator helper testing
"""
from corelibs.debug_handling.dump_data import dump_data
from corelibs.iterator_handling.dict_helpers import mask
from typing import Any
from corelibs_dump_data.dump_data import dump_data
from corelibs.iterator_handling.dict_mask import mask
from corelibs.iterator_handling.dict_helpers import set_entry
def __mask():
@@ -95,11 +97,23 @@ def __mask():
print(f"===> Masked: {dump_data(result)}")
def __set_dict_value_entry():
dict_empty: dict[str, Any] = {}
new = set_entry(dict_empty, 'a.b.c', 1)
print(f"[1] Set dict entry: {dump_data(new)}")
new = set_entry(new, 'dict', {'key': 'value'})
print(f"[2] Set dict entry: {dump_data(new)}")
new = set_entry(new, 'list', [1, 2, 3])
print(f"[3] Set dict entry: {dump_data(new)}")
def main():
"""
Test: corelibs.string_handling.string_helpers
"""
__mask()
__set_dict_value_entry()
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_dump_data.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

@@ -4,7 +4,7 @@
jmes path testing
"""
from corelibs.debug_handling.dump_data import dump_data
from corelibs_dump_data.dump_data import dump_data
from corelibs.json_handling.jmespath_helper import jmespath_search
@@ -39,7 +39,9 @@ def main() -> None:
'a',
'bar.a',
'foo.a',
'baz[].aa'
'baz[].aa',
"[?\"c\" && contains(\"c\", 'b')]",
"[?contains(\"c\", 'b')]",
]
for __jmespath in __get:
result = jmespath_search(__set, __jmespath)

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""
JSON content replace tets
"""
from deepdiff import DeepDiff
from corelibs_dump_data.dump_data import dump_data
from corelibs.json_handling.json_helper import modify_with_jsonpath
def main() -> None:
"""
Comment
"""
__data = {
'a': 'b',
'foobar': [1, 2, 'a'],
'bar': {
'a': 1,
'b': 'c'
},
'baz': [
{
'aa': 1,
'ab': 'cc'
},
{
'ba': 2,
'bb': 'dd'
},
],
'foo': {
'a': [1, 2, 3],
'b': ['a', 'b', 'c']
}
}
# Modify some values using JSONPath
__replace_data = modify_with_jsonpath(__data, 'bar.a', 42)
__replace_data = modify_with_jsonpath(__replace_data, 'foo.b[1]', 'modified')
__replace_data = modify_with_jsonpath(__replace_data, 'baz[0].ab', 'changed')
print(f"Original Data:\n{dump_data(__data)}\n")
print(f"Modified Data:\n{dump_data(__replace_data)}\n")
print(f"Differences:\n{dump_data(DeepDiff(__data, __replace_data, verbose_level=2))}\n")
if __name__ == "__main__":
main()
# __END__

View File

@@ -6,8 +6,8 @@ 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.debug_handling.debug_helpers import exception_stack, call_stack
from corelibs_stack_trace.stack import exception_stack, call_stack
from corelibs.logging_handling.log import Log, Logger, ConsoleFormat, ConsoleFormatSettings
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

@@ -9,8 +9,9 @@ from random import randint
import sys
import io
from pathlib import Path
from corelibs.file_handling.progress import Progress
from corelibs.string_handling.datetime_helpers import convert_timestamp, create_time
from corelibs.script_handling.progress import Progress
from corelibs.datetime_handling.datetime_helpers import create_time
from corelibs.datetime_handling.timestamp_convert import convert_timestamp
def main():
@@ -36,8 +37,8 @@ def main():
# prg.SetStartTime(time.time())
prg.set_start_time()
print(
f"PRECISION: {prg.precision} | TEN STEP: {prg.precision_ten_step} | "
f"WIDE TEME: {prg.wide_time} | MICROTIME: {prg.microtime} | VERBOSE: {prg.verbose}"
f"PRECISION: {prg.__precision} | TEN STEP: {prg.precision_ten_step} | "
f"WIDE TEME: {prg.__wide_time} | MICROTIME: {prg.microtime} | VERBOSE: {prg.verbose}"
)
if use_file:
@@ -55,7 +56,7 @@ def main():
print(
f"Buffer size: {io.DEFAULT_BUFFER_SIZE} | "
f"Do Buffering: {fh.line_buffering} | "
f"File size: {prg.filesize}"
f"File size: {prg.__filesize}"
)
data = fh.readline()
while data:
@@ -71,7 +72,7 @@ def main():
print(f"Starting: {create_time(prg.start if prg.start is not None else 0)}")
prg.set_linecount(256)
i = 1
while i <= prg.linecount:
while i <= prg.__linecount:
sleep = randint(1, 9)
sleep /= 7
time.sleep(sleep)

View File

@@ -0,0 +1,38 @@
"""
Caller tests
"""
from corelibs_dump_data.dump_data import dump_data
from corelibs.requests_handling.caller import Caller, ErrorResponse
from corelibs.requests_handling.auth_helpers import basic_auth
def test_basic_auth():
"""basic auth test"""
user = "user"
password = "pass"
auth_header = basic_auth(user, password)
print(f"Auth Header for '{user}' & '{password}': {auth_header}")
def test_caller():
"""Caller tests"""
caller = Caller()
response = caller.get("https://httpbin.org/get")
if isinstance(response, ErrorResponse):
print(f"Error: {response.message}")
else:
print(f"Response Status Code: {response.status_code}")
print(f"Response Content: {dump_data(response.json())}")
def main():
"""main"""
test_caller()
test_basic_auth()
if __name__ == "__main__":
main()
# __END__

View File

@@ -5,7 +5,7 @@ Test string_handling/string_helpers
import sys
from decimal import Decimal, getcontext
from textwrap import shorten
from corelibs.string_handling.string_helpers import shorten_string, format_number
from corelibs.string_handling.string_helpers import shorten_string, format_number, prepare_url_slash
from corelibs.string_handling.text_colors import Colors
@@ -73,6 +73,18 @@ def __sh_colors():
print(f"Underline/Yellow/Bold: {Colors.underline}{Colors.bold}{Colors.yellow}UNDERLINE YELLOW BOLD{Colors.reset}")
def __prepare_url_slash():
urls = [
"api/v1/resource",
"/api/v1/resource",
"///api//v1//resource//",
"api//v1/resource/",
]
for url in urls:
prepared = prepare_url_slash(url)
print(f"IN: {url} -> OUT: {prepared}")
def main():
"""
Test: corelibs.string_handling.string_helpers
@@ -80,6 +92,7 @@ def main():
__sh_shorten_string()
__sh_format_number()
__sh_colors()
__prepare_url_slash()
if __name__ == "__main__":

View File

@@ -1,54 +0,0 @@
#!/usr/bin/env python3
"""
timestamp string checks
"""
from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError
def main() -> None:
"""
Comment
"""
test_cases = [
"5M 6d", # 5 months, 6 days
"2h 30m 45s", # 2 hours, 30 minutes, 45 seconds
"1Y 2M 3d", # 1 year, 2 months, 3 days
"1h", # 1 hour
"30m", # 30 minutes
"2 hours 15 minutes", # 2 hours, 15 minutes
"1d 12h", # 1 day, 12 hours
"3M 2d 4h", # 3 months, 2 days, 4 hours
"45s", # 45 seconds
"1 year 2 months", # 1 year, 2 months
"2Y 6M 15d 8h 30m 45s", # Complex example
# ]
# invalid_test_cases = [
"5M 6d 2M", # months appears twice
"2h 30m 45s 1h", # hours appears twice
"1d 2 days", # days appears twice (short and long form)
"30m 45 minutes", # minutes appears twice
"1Y 2 years", # years appears twice
"1x 2 yrs", # invalid names
123, # int
789.12, # float
456.56, # float, high
"4566", # int as string
"5551.12", # float as string
"5551.56", # float, high as string
]
for time_string in test_cases:
try:
result = convert_to_seconds(time_string)
print(f"{time_string} => {result}")
except (TimeParseError, TimeUnitError) as e:
print(f"Error encountered for {time_string}: {type(e).__name__}: {e}")
if __name__ == "__main__":
main()
# __END__

View File

@@ -4,10 +4,12 @@
Test for double byte format
"""
from corelibs.string_handling.timestamp_strings import TimestampStrings
from zoneinfo import ZoneInfo
from corelibs.datetime_handling.timestamp_strings import TimestampStrings
def main():
"""test"""
ts = TimestampStrings()
print(f"TS: {ts.timestamp_now}")
@@ -16,6 +18,14 @@ def main():
except ValueError as e:
print(f"Value error: {e}")
ts = TimestampStrings("Europe/Vienna")
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
ts = TimestampStrings(ZoneInfo("Europe/Vienna"))
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
custom_tz = 'Europe/Paris'
ts = TimestampStrings(time_zone=custom_tz)
print(f"TZ: {ts.time_zone} -> TS: {ts.timestamp_now_tz}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""
Enum handling
"""
from corelibs.var_handling.enum_base import EnumBase
class TestBlock(EnumBase):
"""Test block enum"""
BLOCK_A = "block_a"
HAS_NUM = 5
def main() -> None:
"""
Comment
"""
print(f"BLOCK A: {TestBlock.from_any('BLOCK_A')}")
print(f"HAS NUM: {TestBlock.from_any(5)}")
print(f"DIRECT BLOCK: {TestBlock.BLOCK_A.name} -> {TestBlock.BLOCK_A.value}")
if __name__ == "__main__":
main()
# __END__

View File

View File

@@ -0,0 +1,881 @@
"""
Unit tests for SettingsLoader class
"""
import configparser
from pathlib import Path
from unittest.mock import Mock
import pytest
from pytest import CaptureFixture
from corelibs.config_handling.settings_loader import SettingsLoader
from corelibs.logging_handling.log import Log
class TestSettingsLoaderInit:
"""Test cases for SettingsLoader initialization"""
def test_init_with_valid_config_file(self, tmp_path: Path):
"""Test initialization with a valid config file"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
loader = SettingsLoader(
args={},
config_file=config_file,
log=None,
always_print=False
)
assert loader.args == {}
assert loader.config_file == config_file
assert loader.log is None
assert loader.always_print is False
assert loader.config_parser is not None
assert isinstance(loader.config_parser, configparser.ConfigParser)
def test_init_with_missing_config_file(self, tmp_path: Path):
"""Test initialization with missing config file"""
config_file = tmp_path.joinpath("missing.ini")
loader = SettingsLoader(
args={},
config_file=config_file,
log=None,
always_print=False
)
assert loader.config_parser is None
def test_init_with_invalid_config_folder(self):
"""Test initialization with invalid config folder path"""
config_file = Path("/nonexistent/path/test.ini")
with pytest.raises(ValueError, match="Cannot find the config folder"):
SettingsLoader(
args={},
config_file=config_file,
log=None,
always_print=False
)
def test_init_with_log(self, tmp_path: Path):
"""Test initialization with Log object"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[Section]\nkey=value\n")
mock_log = Mock(spec=Log)
loader = SettingsLoader(
args={"test": "value"},
config_file=config_file,
log=mock_log,
always_print=True
)
assert loader.log == mock_log
assert loader.always_print is True
class TestLoadSettings:
"""Test cases for load_settings method"""
def test_load_settings_basic(self, tmp_path: Path):
"""Test loading basic settings without validation"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nkey1=value1\nkey2=value2\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings("TestSection")
assert result == {"key1": "value1", "key2": "value2"}
def test_load_settings_with_missing_section(self, tmp_path: Path):
"""Test loading settings with missing section"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Cannot read \\[MissingSection\\]"):
loader.load_settings("MissingSection")
def test_load_settings_allow_not_exist(self, tmp_path: Path):
"""Test loading settings with allow_not_exist flag"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[OtherSection]\nkey=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings("MissingSection", allow_not_exist=True)
assert result == {}
def test_load_settings_mandatory_field_present(self, tmp_path: Path):
"""Test mandatory field validation when field is present"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"required_field": ["mandatory:yes"]}
)
assert result["required_field"] == "value"
def test_load_settings_mandatory_field_missing(self, tmp_path: Path):
"""Test mandatory field validation when field is missing"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother_field=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"required_field": ["mandatory:yes"]}
)
def test_load_settings_mandatory_field_empty(self, tmp_path: Path):
"""Test mandatory field validation when field is empty"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nrequired_field=\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"required_field": ["mandatory:yes"]}
)
def test_load_settings_with_split(self, tmp_path: Path):
"""Test splitting values into lists"""
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)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:,"]}
)
assert result["list_field"] == ["a", "b", "c", "d"]
def test_load_settings_with_custom_split_char(self, tmp_path: Path):
"""Test splitting with custom delimiter"""
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)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:|"]}
)
assert result["list_field"] == ["a", "b", "c", "d"]
def test_load_settings_split_removes_spaces(self, tmp_path: Path):
"""Test that split removes spaces from values"""
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)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:,"]}
)
assert result["list_field"] == ["a", "b", "c", "d"]
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.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist_field=a,b,c\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"list_field": ["split:"]}
)
assert result["list_field"] == ["a", "b", "c"]
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.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"number": ["convert:int"]}
)
assert result["number"] == 123
assert isinstance(result["number"], int)
def test_load_settings_convert_to_float(self, tmp_path: Path):
"""Test converting values to float"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=123.45\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"number": ["convert:float"]}
)
assert result["number"] == 123.45
assert isinstance(result["number"], float)
def test_load_settings_convert_to_bool_true(self, tmp_path: Path):
"""Test converting values to boolean True"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=true\nflag2=True\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"flag1": ["convert:bool"], "flag2": ["convert:bool"]}
)
assert result["flag1"] is True
assert result["flag2"] is True
def test_load_settings_convert_to_bool_false(self, tmp_path: Path):
"""Test converting values to boolean False"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nflag1=false\nflag2=False\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"flag1": ["convert:bool"], "flag2": ["convert:bool"]}
)
assert result["flag1"] is False
assert result["flag2"] is False
def test_load_settings_convert_invalid_type(self, tmp_path: Path):
"""Test converting with invalid type raises error"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="convert type is invalid"):
loader.load_settings(
"TestSection",
{"value": ["convert:invalid"]}
)
def test_load_settings_empty_set_to_none(self, tmp_path: Path):
"""Test setting empty values to None"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"field": ["empty:"]}
)
assert result["field"] is None
def test_load_settings_empty_set_to_custom_value(self, tmp_path: Path):
"""Test setting empty values to custom value"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nother=value\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"field": ["empty:default"]}
)
assert result["field"] == "default"
def test_load_settings_matching_valid(self, tmp_path: Path):
"""Test matching validation with valid value"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=production\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"mode": ["matching:development|staging|production"]}
)
assert result["mode"] == "production"
def test_load_settings_matching_invalid(self, tmp_path: Path):
"""Test matching validation with invalid value"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nmode=invalid\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"mode": ["matching:development|staging|production"]}
)
def test_load_settings_in_valid(self, tmp_path: Path):
"""Test 'in' validation with valid value"""
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)
result = loader.load_settings(
"TestSection",
{
"allowed": ["split:,"],
"value": ["in:allowed"]
}
)
assert result["value"] == "b"
def test_load_settings_in_invalid(self, tmp_path: Path):
"""Test 'in' validation with invalid value"""
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)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{
"allowed": ["split:,"],
"value": ["in:allowed"]
}
)
def test_load_settings_in_missing_target(self, tmp_path: Path):
"""Test 'in' validation with missing target"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=a\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"value": ["in:missing_target"]}
)
def test_load_settings_length_exact(self, tmp_path: Path):
"""Test length validation with exact match"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"value": ["length:4"]}
)
assert result["value"] == "test"
def test_load_settings_length_exact_invalid(self, tmp_path: Path):
"""Test length validation with exact match failure"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"value": ["length:5"]}
)
def test_load_settings_length_range(self, tmp_path: Path):
"""Test length validation with range"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"value": ["length:5-10"]}
)
assert result["value"] == "testing"
def test_load_settings_length_min_only(self, tmp_path: Path):
"""Test length validation with minimum only"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=testing\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"value": ["length:5-"]}
)
assert result["value"] == "testing"
def test_load_settings_length_max_only(self, tmp_path: Path):
"""Test length validation with maximum only"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"value": ["length:-10"]}
)
assert result["value"] == "test"
def test_load_settings_range_valid(self, tmp_path: Path):
"""Test range validation with valid value"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=25\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"number": ["range:10-50"]}
)
assert result["number"] == "25"
def test_load_settings_range_invalid(self, tmp_path: Path):
"""Test range validation with invalid value"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=100\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"number": ["range:10-50"]}
)
def test_load_settings_check_int_valid(self, tmp_path: Path):
"""Test check:int with valid integer"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12345\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"number": ["check:int"]}
)
assert result["number"] == "12345"
def test_load_settings_check_int_cleanup(self, tmp_path: Path):
"""Test check:int with cleanup"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nnumber=12a34b5\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"number": ["check:int"]}
)
assert result["number"] == "12345"
def test_load_settings_check_email_valid(self, tmp_path: Path):
"""Test check:string.email.basic with valid email"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=test@example.com\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"email": ["check:string.email.basic"]}
)
assert result["email"] == "test@example.com"
def test_load_settings_check_email_invalid(self, tmp_path: Path):
"""Test check:string.email.basic with invalid email"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nemail=not-an-email\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Missing or incorrect settings data"):
loader.load_settings(
"TestSection",
{"email": ["check:string.email.basic"]}
)
def test_load_settings_args_override(self, tmp_path: Path, capsys: CaptureFixture[str]):
"""Test command line arguments override config values"""
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": []}
)
assert result["value"] == "arg_value"
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.joinpath("missing.ini")
loader = SettingsLoader(
args={"required": "value"},
config_file=config_file
)
result = loader.load_settings(
"TestSection",
{"required": ["mandatory:yes"]}
)
assert result["required"] == "value"
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.joinpath("missing.ini")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Cannot find file"):
loader.load_settings(
"TestSection",
{"required": ["mandatory:yes"]}
)
def test_load_settings_check_list_with_split(self, tmp_path: Path):
"""Test check validation with list values"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nlist=abc,def,ghi\n")
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"TestSection",
{"list": ["split:,", "check:string.alphanumeric"]}
)
assert result["list"] == ["abc", "def", "ghi"]
def test_load_settings_check_list_cleanup(self, tmp_path: Path):
"""Test check validation cleans up list values"""
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)
result = loader.load_settings(
"TestSection",
{"list": ["split:,", "check:string.alphanumeric"]}
)
assert result["list"] == ["abc", "def", "ghi"]
def test_load_settings_invalid_check_type(self, tmp_path: Path):
"""Test with invalid check type"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text("[TestSection]\nvalue=test\n")
loader = SettingsLoader(args={}, config_file=config_file)
with pytest.raises(ValueError, match="Cannot get SettingsLoaderCheck.CHECK_SETTINGS"):
loader.load_settings(
"TestSection",
{"value": ["check:invalid.check.type"]}
)
class TestComplexScenarios:
"""Test cases for complex real-world scenarios"""
def test_complex_validation_scenario(self, tmp_path: Path):
"""Test complex scenario with multiple validations"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Production]\n"
"environment=production\n"
"allowed_envs=development,staging,production\n"
"port=8080\n"
"host=example.com\n"
"timeout=30\n"
"debug=false\n"
"features=auth,logging,monitoring\n"
)
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"Production",
{
"environment": [
"mandatory:yes",
"matching:development|staging|production",
"in:allowed_envs"
],
"allowed_envs": ["split:,"],
"port": ["mandatory:yes", "convert:int", "range:1-65535"],
"host": ["mandatory:yes"],
"timeout": ["convert:int", "range:1-"],
"debug": ["convert:bool"],
"features": ["split:,", "check:string.alphanumeric"],
}
)
assert result["environment"] == "production"
assert result["allowed_envs"] == ["development", "staging", "production"]
assert result["port"] == 8080
assert isinstance(result["port"], int)
assert result["host"] == "example.com"
assert result["timeout"] == 30
assert result["debug"] is False
assert result["features"] == ["auth", "logging", "monitoring"]
def test_email_list_validation(self, tmp_path: Path):
"""Test email list with validation"""
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"
)
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"EmailConfig",
{"emails": ["split:,", "mandatory:yes", "check:string.email.basic"]}
)
assert len(result["emails"]) == 3
assert "test@example.com" in result["emails"]
def test_mixed_args_and_config(self, tmp_path: Path):
"""Test mixing command line args and config file"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Settings]\n"
"value1=config_value1\n"
"value2=config_value2\n"
)
loader = SettingsLoader(
args={"value1": "arg_value1"},
config_file=config_file
)
result = loader.load_settings(
"Settings",
{"value1": [], "value2": []}
)
assert result["value1"] == "arg_value1" # Overridden by arg
assert result["value2"] == "config_value2" # From config
def test_multiple_check_types(self, tmp_path: Path):
"""Test multiple different check types"""
config_file = tmp_path.joinpath("test.ini")
config_file.write_text(
"[Checks]\n"
"numbers=123,456,789\n"
"alphas=abc,def,ghi\n"
"emails=test@example.com\n"
"date=2025-01-15\n"
)
loader = SettingsLoader(args={}, config_file=config_file)
result = loader.load_settings(
"Checks",
{
"numbers": ["split:,", "check:int"],
"alphas": ["split:,", "check:string.alphanumeric"],
"emails": ["check:string.email.basic"],
"date": ["check:string.date"],
}
)
assert result["numbers"] == ["123", "456", "789"]
assert result["alphas"] == ["abc", "def", "ghi"]
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

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,291 +0,0 @@
"""
tests for corelibs.iterator_handling.dict_helpers
"""
import pytest
from typing import Any
from corelibs.iterator_handling.dict_helpers import mask
def test_mask_default_behavior():
"""Test masking with default mask_keys"""
data = {
"username": "john_doe",
"password": "secret123",
"email": "john@example.com",
"api_secret": "abc123",
"encryption_key": "xyz789"
}
result = mask(data)
assert result["username"] == "john_doe"
assert result["password"] == "***"
assert result["email"] == "john@example.com"
assert result["api_secret"] == "***"
assert result["encryption_key"] == "***"
def test_mask_custom_keys():
"""Test masking with custom mask_keys"""
data = {
"username": "john_doe",
"token": "abc123",
"api_key": "xyz789",
"password": "secret123"
}
result = mask(data, mask_keys=["token", "api"])
assert result["username"] == "john_doe"
assert result["token"] == "***"
assert result["api_key"] == "***"
assert result["password"] == "secret123" # Not masked with custom keys
def test_mask_custom_mask_string():
"""Test masking with custom mask string"""
data = {"password": "secret123"}
result = mask(data, mask_str="[HIDDEN]")
assert result["password"] == "[HIDDEN]"
def test_mask_case_insensitive():
"""Test that masking is case insensitive"""
data = {
"PASSWORD": "secret123",
"Secret_Key": "abc123",
"ENCRYPTION_data": "xyz789"
}
result = mask(data)
assert result["PASSWORD"] == "***"
assert result["Secret_Key"] == "***"
assert result["ENCRYPTION_data"] == "***"
def test_mask_key_patterns():
"""Test different key matching patterns (start, end, contains)"""
data = {
"password_hash": "hash123", # starts with
"user_password": "secret123", # ends with
"my_secret_key": "abc123", # contains with edges
"secretvalue": "xyz789", # contains without edges
"startsecretvalue": "xyz123", # contains without edges
"normal_key": "normal_value"
}
result = mask(data)
assert result["password_hash"] == "***"
assert result["user_password"] == "***"
assert result["my_secret_key"] == "***"
assert result["secretvalue"] == "***" # will mask beacuse starts with
assert result["startsecretvalue"] == "xyz123" # will not mask
assert result["normal_key"] == "normal_value"
def test_mask_custom_edges():
"""Test masking with custom edge characters"""
data = {
"my-secret-key": "abc123",
"my_secret_key": "xyz789"
}
result = mask(data, mask_str_edges="-")
assert result["my-secret-key"] == "***"
assert result["my_secret_key"] == "xyz789" # Underscore edges don't match
def test_mask_empty_edges():
"""Test masking with empty edge characters (substring matching)"""
data = {
"secretvalue": "abc123",
"mysecretkey": "xyz789",
"normal_key": "normal_value"
}
result = mask(data, mask_str_edges="")
assert result["secretvalue"] == "***"
assert result["mysecretkey"] == "***"
assert result["normal_key"] == "normal_value"
def test_mask_nested_dict():
"""Test masking nested dictionaries"""
data = {
"user": {
"name": "john",
"password": "secret123",
"profile": {
"email": "john@example.com",
"encryption_key": "abc123"
}
},
"api_secret": "xyz789"
}
result = mask(data)
assert result["user"]["name"] == "john"
assert result["user"]["password"] == "***"
assert result["user"]["profile"]["email"] == "john@example.com"
assert result["user"]["profile"]["encryption_key"] == "***"
assert result["api_secret"] == "***"
def test_mask_lists():
"""Test masking lists and nested structures with lists"""
data = {
"users": [
{"name": "john", "password": "secret1"},
{"name": "jane", "password": "secret2"}
],
"secrets": ["secret1", "secret2", "secret3"]
}
result = mask(data)
print(f"R {result['secrets']}")
assert result["users"][0]["name"] == "john"
assert result["users"][0]["password"] == "***"
assert result["users"][1]["name"] == "jane"
assert result["users"][1]["password"] == "***"
assert result["secrets"] == ["***", "***", "***"]
def test_mask_mixed_types():
"""Test masking with different value types"""
data = {
"password": "string_value",
"secret_number": 12345,
"encryption_flag": True,
"secret_float": 3.14,
"password_none": None,
"normal_key": "normal_value"
}
result = mask(data)
assert result["password"] == "***"
assert result["secret_number"] == "***"
assert result["encryption_flag"] == "***"
assert result["secret_float"] == "***"
assert result["password_none"] == "***"
assert result["normal_key"] == "normal_value"
def test_mask_skip_true():
"""Test that skip=True returns original data unchanged"""
data = {
"password": "secret123",
"encryption_key": "abc123",
"normal_key": "normal_value"
}
result = mask(data, skip=True)
assert result == data
assert result is data # Should return the same object
def test_mask_empty_dict():
"""Test masking empty dictionary"""
data: dict[str, Any] = {}
result = mask(data)
assert result == {}
def test_mask_none_mask_keys():
"""Test explicit None mask_keys uses defaults"""
data = {"password": "secret123", "token": "abc123"}
result = mask(data, mask_keys=None)
assert result["password"] == "***"
assert result["token"] == "abc123" # Not in default keys
def test_mask_empty_mask_keys():
"""Test empty mask_keys list"""
data = {"password": "secret123", "secret": "abc123"}
result = mask(data, mask_keys=[])
assert result["password"] == "secret123"
assert result["secret"] == "abc123"
def test_mask_complex_nested_structure():
"""Test masking complex nested structure"""
data = {
"config": {
"database": {
"host": "localhost",
"password": "db_secret",
"users": [
{"name": "admin", "password": "admin123"},
{"name": "user", "secret_key": "user456"}
]
},
"api": {
"endpoints": ["api1", "api2"],
"encryption_settings": {
"enabled": True,
"secret": "api_secret"
}
}
}
}
result = mask(data)
assert result["config"]["database"]["host"] == "localhost"
assert result["config"]["database"]["password"] == "***"
assert result["config"]["database"]["users"][0]["name"] == "admin"
assert result["config"]["database"]["users"][0]["password"] == "***"
assert result["config"]["database"]["users"][1]["name"] == "user"
assert result["config"]["database"]["users"][1]["secret_key"] == "***"
assert result["config"]["api"]["endpoints"] == ["api1", "api2"]
assert result["config"]["api"]["encryption_settings"]["enabled"] is True
assert result["config"]["api"]["encryption_settings"]["secret"] == "***"
def test_mask_preserves_original_data():
"""Test that original data is not modified"""
original_data = {
"password": "secret123",
"username": "john_doe"
}
data_copy = original_data.copy()
result = mask(original_data)
assert original_data == data_copy # Original unchanged
assert result != original_data # Result is different
assert result["password"] == "***"
assert original_data["password"] == "secret123"
@pytest.mark.parametrize("mask_key,expected_keys", [
(["pass"], ["password", "user_pass", "my_pass_key"]),
(["key"], ["api_key", "secret_key", "my_key_value"]),
(["token"], ["token", "auth_token", "my_token_here"]),
])
def test_mask_parametrized_keys(mask_key: list[str], expected_keys: list[str]):
"""Parametrized test for different mask key patterns"""
data = {key: "value" for key in expected_keys}
data["normal_entry"] = "normal_value"
result = mask(data, mask_keys=mask_key)
for key in expected_keys:
assert result[key] == "***"
assert result["normal_entry"] == "normal_value"

View File

@@ -1,300 +0,0 @@
"""
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
class TestConvertToList:
"""Test cases for convert_to_list function"""
def test_string_input(self):
"""Test with string inputs"""
assert convert_to_list("hello") == ["hello"]
assert convert_to_list("") == [""]
assert convert_to_list("123") == ["123"]
assert convert_to_list("true") == ["true"]
def test_integer_input(self):
"""Test with integer inputs"""
assert convert_to_list(42) == [42]
assert convert_to_list(0) == [0]
assert convert_to_list(-10) == [-10]
assert convert_to_list(999999) == [999999]
def test_float_input(self):
"""Test with float inputs"""
assert convert_to_list(3.14) == [3.14]
assert convert_to_list(0.0) == [0.0]
assert convert_to_list(-2.5) == [-2.5]
assert convert_to_list(1.0) == [1.0]
def test_boolean_input(self):
"""Test with boolean inputs"""
assert convert_to_list(True) == [True]
assert convert_to_list(False) == [False]
def test_list_input_unchanged(self):
"""Test that list inputs are returned unchanged"""
# String lists
str_list = ["a", "b", "c"]
assert convert_to_list(str_list) == str_list
assert convert_to_list(str_list) is str_list # Same object reference
# Integer lists
int_list = [1, 2, 3]
assert convert_to_list(int_list) == int_list
assert convert_to_list(int_list) is int_list
# Float lists
float_list = [1.1, 2.2, 3.3]
assert convert_to_list(float_list) == float_list
assert convert_to_list(float_list) is float_list
# Boolean lists
bool_list = [True, False, True]
assert convert_to_list(bool_list) == bool_list
assert convert_to_list(bool_list) is bool_list
# Mixed lists
mixed_list = [1, "hello", 3.14, True]
assert convert_to_list(mixed_list) == mixed_list
assert convert_to_list(mixed_list) is mixed_list
# Empty list
empty_list: list[int] = []
assert convert_to_list(empty_list) == empty_list
assert convert_to_list(empty_list) is empty_list
def test_nested_lists(self):
"""Test with nested lists (should still return the same list)"""
nested_list: list[list[int]] = [[1, 2], [3, 4]]
assert convert_to_list(nested_list) == nested_list
assert convert_to_list(nested_list) is nested_list
def test_single_element_lists(self):
"""Test with single element lists"""
single_str = ["hello"]
assert convert_to_list(single_str) == single_str
assert convert_to_list(single_str) is single_str
single_int = [42]
assert convert_to_list(single_int) == single_int
assert convert_to_list(single_int) is single_int
class TestIsListInList:
"""Test cases for is_list_in_list function"""
def test_string_lists(self):
"""Test with string lists"""
list_a = ["a", "b", "c", "d"]
list_b = ["b", "d", "e"]
result = is_list_in_list(list_a, list_b)
assert set(result) == {"a", "c"}
assert isinstance(result, list)
def test_integer_lists(self):
"""Test with integer lists"""
list_a = [1, 2, 3, 4, 5]
list_b = [2, 4, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 3, 5}
assert isinstance(result, list)
def test_float_lists(self):
"""Test with float lists"""
list_a = [1.1, 2.2, 3.3, 4.4]
list_b = [2.2, 4.4, 5.5]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1.1, 3.3}
assert isinstance(result, list)
def test_boolean_lists(self):
"""Test with boolean lists"""
list_a = [True, False, True]
list_b = [True]
result = is_list_in_list(list_a, list_b)
assert set(result) == {False}
assert isinstance(result, list)
def test_mixed_type_lists(self):
"""Test with mixed type lists"""
list_a = [1, "hello", 3.14, True, "world"]
list_b = ["hello", True, 42]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 3.14, "world"}
assert isinstance(result, list)
def test_empty_lists(self):
"""Test with empty lists"""
# Empty list_a
assert is_list_in_list([], [1, 2, 3]) == []
# Empty list_b
list_a = [1, 2, 3]
result = is_list_in_list(list_a, [])
assert set(result) == {1, 2, 3}
# Both empty
assert is_list_in_list([], []) == []
def test_no_common_elements(self):
"""Test when lists have no common elements"""
list_a = [1, 2, 3]
list_b = [4, 5, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1, 2, 3}
def test_all_elements_common(self):
"""Test when all elements in list_a are in list_b"""
list_a = [1, 2, 3]
list_b = [1, 2, 3, 4, 5]
result = is_list_in_list(list_a, list_b)
assert result == []
def test_identical_lists(self):
"""Test with identical lists"""
list_a = [1, 2, 3]
list_b = [1, 2, 3]
result = is_list_in_list(list_a, list_b)
assert result == []
def test_duplicate_elements(self):
"""Test with duplicate elements in lists"""
list_a = [1, 2, 2, 3, 3, 3]
list_b = [2, 4]
result = is_list_in_list(list_a, list_b)
# Should return unique elements only (set behavior)
assert set(result) == {1, 3}
assert isinstance(result, list)
def test_list_b_larger_than_list_a(self):
"""Test when list_b is larger than list_a"""
list_a = [1, 2]
list_b = [2, 3, 4, 5, 6, 7, 8]
result = is_list_in_list(list_a, list_b)
assert set(result) == {1}
def test_order_independence(self):
"""Test that order doesn't matter due to set operations"""
list_a = [3, 1, 4, 1, 5]
list_b = [1, 2, 6]
result = is_list_in_list(list_a, list_b)
assert set(result) == {3, 4, 5}
# Parametrized tests for more comprehensive coverage
class TestParametrized:
"""Parametrized tests for better coverage"""
@pytest.mark.parametrize("input_value,expected", [
("hello", ["hello"]),
(42, [42]),
(3.14, [3.14]),
(True, [True]),
(False, [False]),
("", [""]),
(0, [0]),
(0.0, [0.0]),
(-1, [-1]),
(-2.5, [-2.5]),
])
def test_convert_to_list_parametrized(self, input_value: Any, expected: Any):
"""Test convert_to_list with various single values"""
assert convert_to_list(input_value) == expected
@pytest.mark.parametrize("input_list", [
[1, 2, 3],
["a", "b", "c"],
[1.1, 2.2, 3.3],
[True, False],
[1, "hello", 3.14, True],
[],
[42],
[[1, 2], [3, 4]],
])
def test_convert_to_list_with_lists_parametrized(self, input_list: Any):
"""Test convert_to_list with various list inputs"""
result = convert_to_list(input_list)
assert result == input_list
assert result is input_list # Same object reference
@pytest.mark.parametrize("list_a,list_b,expected_set", [
([1, 2, 3], [2], {1, 3}),
(["a", "b", "c"], ["b", "d"], {"a", "c"}),
([1, 2, 3], [4, 5, 6], {1, 2, 3}),
([1, 2, 3], [1, 2, 3], set()),
([], [1, 2, 3], set()),
([1, 2, 3], [], {1, 2, 3}),
([True, False], [True], {False}),
([1.1, 2.2, 3.3], [2.2], {1.1, 3.3}),
])
def test_is_list_in_list_parametrized(self, list_a: list[Any], list_b: list[Any], expected_set: Any):
"""Test is_list_in_list with various input combinations"""
result = is_list_in_list(list_a, list_b)
assert set(result) == expected_set
assert isinstance(result, list)
# Edge cases and special scenarios
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_convert_to_list_with_none_like_values(self):
"""Test convert_to_list with None-like values (if function supports them)"""
# Note: Based on type hints, None is not supported, but testing behavior
# This test might need to be adjusted based on actual function behavior
pass
def test_is_list_in_list_preserves_type_distinctions(self):
"""Test that different types are treated as different"""
list_a = [1, "1", 1.0, True]
list_b = [1] # Only integer 1
result = is_list_in_list(list_a, list_b)
# Note: This test depends on how Python's set handles type equality
# 1, 1.0, and True are considered equal in sets
# "1" is different from 1
# expected_items = {"1"} # String "1" should remain
assert "1" in result
assert isinstance(result, list)
def test_large_lists(self):
"""Test with large lists"""
large_list_a = list(range(1000))
large_list_b = list(range(500, 1500))
result = is_list_in_list(large_list_a, large_list_b)
expected = list(range(500)) # 0 to 499
assert set(result) == set(expected)
def test_memory_efficiency(self):
"""Test that convert_to_list doesn't create unnecessary copies"""
original_list = [1, 2, 3, 4, 5]
result = convert_to_list(original_list)
# Should be the same object, not a copy
assert result is original_list
# Modifying the original should affect the result
original_list.append(6)
assert 6 in result
# Performance tests (optional)
class TestPerformance:
"""Performance-related tests"""
def test_is_list_in_list_with_duplicates_performance(self):
"""Test that function handles duplicates efficiently"""
# List with many duplicates
list_a = [1, 2, 3] * 100 # 300 elements, many duplicates
list_b = [2] * 50 # 50 elements, all the same
result = is_list_in_list(list_a, list_b)
# Should still work correctly despite duplicates
assert set(result) == {1, 3}
assert isinstance(result, list)

View File

@@ -0,0 +1,332 @@
"""
Unit tests for log settings parsing and spacer constants in Log class.
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
from pathlib import Path
from typing import Any
import pytest
from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test Log Settings Parsing
class TestLogSettingsParsing:
"""Test cases for log settings parsing"""
def test_parse_with_string_log_levels(self, tmp_log_path: Path):
"""Test parsing with string log levels"""
settings: dict[str, Any] = {
"log_level_console": "ERROR",
"log_level_file": "INFO",
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["log_level_console"] == LoggingLevel.ERROR
assert log.log_settings["log_level_file"] == LoggingLevel.INFO
def test_parse_with_int_log_levels(self, tmp_log_path: Path):
"""Test parsing with integer log levels"""
settings: dict[str, Any] = {
"log_level_console": 40, # ERROR
"log_level_file": 20, # INFO
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
assert log.log_settings["log_level_console"] == LoggingLevel.ERROR
assert log.log_settings["log_level_file"] == LoggingLevel.INFO
def test_parse_with_invalid_bool_settings(self, tmp_log_path: Path):
"""Test parsing with invalid bool settings"""
settings: dict[str, Any] = {
"console_enabled": "not_a_bool",
"per_run_log": 123,
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings # type: ignore
)
# Should fall back to defaults
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:
"""Test cases for spacer constants"""
def test_spacer_char_constant(self):
"""Test SPACER_CHAR constant"""
assert Log.SPACER_CHAR == '='
assert LogParent.SPACER_CHAR == '='
def test_spacer_length_constant(self):
"""Test SPACER_LENGTH constant"""
assert Log.SPACER_LENGTH == 32
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"""
@pytest.mark.parametrize("log_level,expected", [
(LoggingLevel.DEBUG, 10),
(LoggingLevel.INFO, 20),
(LoggingLevel.WARNING, 30),
(LoggingLevel.ERROR, 40),
(LoggingLevel.CRITICAL, 50),
(LoggingLevel.ALERT, 55),
(LoggingLevel.EMERGENCY, 60),
(LoggingLevel.EXCEPTION, 70),
])
def test_log_level_values(self, log_level: LoggingLevel, expected: int):
"""Test log level values"""
assert log_level.value == expected
@pytest.mark.parametrize("method_name,level_name", [
("debug", "DEBUG"),
("info", "INFO"),
("warning", "WARNING"),
("error", "ERROR"),
("critical", "CRITICAL"),
])
def test_logging_methods_write_correct_level(
self,
log_instance: Log,
tmp_log_path: Path,
method_name: str,
level_name: str
):
"""Test each logging method writes correct level"""
method = getattr(log_instance, method_name)
method(f"Test {level_name} message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert level_name in content
assert f"Test {level_name} message" in content
@pytest.mark.parametrize("setting_key,valid_value,invalid_value", [
("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, {}),
])
def test_bool_setting_validation(
self,
tmp_log_path: Path,
setting_key: str,
valid_value: bool,
invalid_value: Any
):
"""Test bool setting validation and fallback"""
# Test with valid value
settings_valid: dict[str, Any] = {setting_key: valid_value}
log_valid = Log(tmp_log_path, "test_valid", settings_valid) # type: ignore
assert log_valid.log_settings[setting_key] == valid_value
# Test with invalid value (should fall back to default)
settings_invalid: dict[str, Any] = {setting_key: invalid_value}
log_invalid = Log(tmp_log_path, "test_invalid", settings_invalid) # type: ignore
assert log_invalid.log_settings[setting_key] == Log.DEFAULT_LOG_SETTINGS.get(
setting_key, True
)
# __END__

View File

@@ -0,0 +1,518 @@
"""
Unit tests for basic Log handling functionality.
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
import logging
from pathlib import Path
from typing import Any
import pytest
from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test LogParent
class TestLogParent:
"""Test cases for LogParent class"""
def test_validate_log_level_valid(self):
"""Test validate_log_level with valid levels"""
assert LogParent.validate_log_level(LoggingLevel.DEBUG) is True
assert LogParent.validate_log_level(10) is True
assert LogParent.validate_log_level("INFO") is True
assert LogParent.validate_log_level("warning") is True
def test_validate_log_level_invalid(self):
"""Test validate_log_level with invalid levels"""
assert LogParent.validate_log_level("INVALID") is False
assert LogParent.validate_log_level(999) is False
def test_get_log_level_int_valid(self):
"""Test get_log_level_int with valid levels"""
assert LogParent.get_log_level_int(LoggingLevel.DEBUG) == 10
assert LogParent.get_log_level_int(20) == 20
assert LogParent.get_log_level_int("ERROR") == 40
def test_get_log_level_int_invalid(self):
"""Test get_log_level_int with invalid level returns default"""
result = LogParent.get_log_level_int("INVALID")
assert result == LoggingLevel.WARNING.value
def test_debug_without_logger_raises(self):
"""Test debug method raises when logger not initialized"""
parent = LogParent()
with pytest.raises(ValueError, match="Logger is not yet initialized"):
parent.debug("Test message")
def test_info_without_logger_raises(self):
"""Test info method raises when logger not initialized"""
parent = LogParent()
with pytest.raises(ValueError, match="Logger is not yet initialized"):
parent.info("Test message")
def test_warning_without_logger_raises(self):
"""Test warning method raises when logger not initialized"""
parent = LogParent()
with pytest.raises(ValueError, match="Logger is not yet initialized"):
parent.warning("Test message")
def test_error_without_logger_raises(self):
"""Test error method raises when logger not initialized"""
parent = LogParent()
with pytest.raises(ValueError, match="Logger is not yet initialized"):
parent.error("Test message")
def test_critical_without_logger_raises(self):
"""Test critical method raises when logger not initialized"""
parent = LogParent()
with pytest.raises(ValueError, match="Logger is not yet initialized"):
parent.critical("Test message")
def test_flush_without_queue_returns_false(self, log_instance: Log):
"""Test flush returns False when no queue"""
result = log_instance.flush()
assert result is False
def test_cleanup_without_queue(self, log_instance: Log):
"""Test cleanup does nothing when no queue"""
log_instance.cleanup() # Should not raise
# MARK: Test Log Initialization
class TestLogInitialization:
"""Test cases for Log class initialization"""
def test_init_basic(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test basic Log initialization"""
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
assert log.log_name == "test_log"
assert log.logger is not None
assert isinstance(log.logger, logging.Logger)
assert "file_handler" in log.handlers
assert "stream_handler" in log.handlers
def test_init_with_log_extension(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test initialization with .log extension in name"""
log = Log(
log_path=tmp_log_path,
log_name="test_log.log",
log_settings=basic_log_settings
)
# When log_name ends with .log, the code strips it but the logic keeps it
# Based on code: if not log_name.endswith('.log'): log_name = Path(log_name).stem
# So if it DOES end with .log, it keeps the original name
assert log.log_name == "test_log.log"
def test_init_with_file_path(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test initialization with file path instead of directory"""
log_file = tmp_log_path / "custom.log"
log = Log(
log_path=log_file,
log_name="test",
log_settings=basic_log_settings
)
assert log.logger is not None
assert log.log_name == "test"
def test_init_console_disabled(self, tmp_log_path: Path):
"""Test initialization with console disabled"""
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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,
}
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings
)
assert "stream_handler" not in log.handlers
assert "file_handler" in log.handlers
def test_init_per_run_log(self, tmp_log_path: Path):
"""Test initialization with per_run_log enabled"""
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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,
}
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings
)
assert log.logger is not None
# Check that a timestamped log file was created
# Files are created in parent directory with sanitized name
log_files = list(tmp_log_path.glob("testlog.*.log"))
assert len(log_files) > 0
def test_init_with_none_settings(self, tmp_log_path: Path):
"""Test initialization with None settings uses defaults"""
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=None
)
assert log.log_settings == Log.DEFAULT_LOG_SETTINGS
assert log.logger is not None
def test_init_with_partial_settings(self, tmp_log_path: Path):
"""Test initialization with partial settings"""
settings: dict[str, Any] = {
"log_level_console": LoggingLevel.ERROR,
"console_enabled": True,
}
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings # type: ignore
)
assert log.log_settings["log_level_console"] == LoggingLevel.ERROR
# Other settings should use defaults
assert log.log_settings["log_level_file"] == Log.DEFAULT_LOG_LEVEL_FILE
def test_init_with_invalid_log_level(self, tmp_log_path: Path):
"""Test initialization with invalid log level falls back to default"""
settings: dict[str, Any] = {
"log_level_console": "INVALID_LEVEL",
}
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings # type: ignore
)
# Invalid log levels are reset to the default for that specific entry
# Since INVALID_LEVEL fails validation, it uses DEFAULT_LOG_SETTINGS value
assert log.log_settings["log_level_console"] == Log.DEFAULT_LOG_SETTINGS["log_level_console"]
def test_init_with_color_output(self, tmp_log_path: Path):
"""Test initialization with color output enabled"""
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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,
}
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=settings
)
console_handler = log.handlers["stream_handler"]
assert isinstance(console_handler.formatter, CustomConsoleFormatter)
def test_init_with_other_handlers(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test initialization with additional custom handlers"""
custom_handler = logging.StreamHandler()
custom_handler.set_name("custom_handler")
log = Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings,
other_handlers={"custom": custom_handler}
)
assert "custom" in log.handlers
assert log.handlers["custom"] == custom_handler
# MARK: Test Log Methods
class TestLogMethods:
"""Test cases for Log logging methods"""
def test_debug_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test debug level logging"""
log_instance.debug("Debug message")
# Verify log file contains the message
# Log file is created with sanitized name (testlog.log)
log_file = tmp_log_path / "testlog.log"
assert log_file.exists()
content = log_file.read_text()
assert "Debug message" in content
assert "DEBUG" in content
def test_info_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test info level logging"""
log_instance.info("Info message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Info message" in content
assert "INFO" in content
def test_warning_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test warning level logging"""
log_instance.warning("Warning message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Warning message" in content
assert "WARNING" in content
def test_error_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test error level logging"""
log_instance.error("Error message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Error message" in content
assert "ERROR" in content
def test_critical_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test critical level logging"""
log_instance.critical("Critical message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Critical message" in content
assert "CRITICAL" in content
def test_alert_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test alert level logging"""
log_instance.alert("Alert message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Alert message" in content
assert "ALERT" in content
def test_emergency_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test emergency level logging"""
log_instance.emergency("Emergency message")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Emergency message" in content
assert "EMERGENCY" in content
def test_exception_logging(self, log_instance: Log, tmp_log_path: Path):
"""Test exception level logging"""
try:
raise ValueError("Test exception")
except ValueError:
log_instance.exception("Exception occurred")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Exception occurred" in content
assert "EXCEPTION" in content
assert "ValueError" in content
def test_exception_logging_without_error(self, log_instance: Log, tmp_log_path: Path):
"""Test exception logging with log_error=False"""
try:
raise ValueError("Test exception")
except ValueError:
log_instance.exception("Exception occurred", log_error=False)
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Exception occurred" in content
# Should not have the ERROR level entry
assert "<=EXCEPTION=" not in content
def test_log_with_extra(self, log_instance: Log, tmp_log_path: Path):
"""Test logging with extra parameters"""
extra: dict[str, object] = {"custom_field": "custom_value"}
log_instance.info("Info with extra", extra=extra)
log_file = tmp_log_path / "testlog.log"
assert log_file.exists()
content = log_file.read_text()
assert "Info with extra" in content
def test_break_line(self, log_instance: Log, tmp_log_path: Path):
"""Test break_line method"""
log_instance.break_line("TEST")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "[TEST]" in content
assert "=" in content
def test_break_line_default(self, log_instance: Log, tmp_log_path: Path):
"""Test break_line with default parameter"""
log_instance.break_line()
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "[BREAK]" in content
# MARK: Test Log Level Handling
class TestLogLevelHandling:
"""Test cases for log level handling"""
def test_set_log_level_file_handler(self, log_instance: Log):
"""Test setting log level for file handler"""
result = log_instance.set_log_level("file_handler", LoggingLevel.ERROR)
assert result is True
assert log_instance.get_log_level("file_handler") == LoggingLevel.ERROR
def test_set_log_level_console_handler(self, log_instance: Log):
"""Test setting log level for console handler"""
result = log_instance.set_log_level("stream_handler", LoggingLevel.CRITICAL)
assert result is True
assert log_instance.get_log_level("stream_handler") == LoggingLevel.CRITICAL
def test_set_log_level_invalid_handler(self, log_instance: Log):
"""Test setting log level for non-existent handler raises KeyError"""
# The actual implementation uses dict access which raises KeyError, not IndexError
with pytest.raises(KeyError):
log_instance.set_log_level("nonexistent", LoggingLevel.DEBUG)
def test_get_log_level_invalid_handler(self, log_instance: Log):
"""Test getting log level for non-existent handler raises KeyError"""
# The actual implementation uses dict access which raises KeyError, not IndexError
with pytest.raises(KeyError):
log_instance.get_log_level("nonexistent")
def test_get_log_level(self, log_instance: Log):
"""Test getting current log level"""
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

@@ -0,0 +1,362 @@
"""
Unit tests for CustomConsoleFormatter in logging handling
"""
# pylint: disable=protected-access,redefined-outer-name
import logging
from pathlib import Path
import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomConsoleFormatter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test CustomConsoleFormatter
class TestCustomConsoleFormatter:
"""Test cases for CustomConsoleFormatter"""
def test_format_debug_level(self):
"""Test formatting DEBUG level message"""
formatter = CustomConsoleFormatter('[%(levelname)s] %(message)s')
record = logging.LogRecord(
name="test",
level=logging.DEBUG,
pathname="test.py",
lineno=1,
msg="Debug message",
args=(),
exc_info=None
)
result = formatter.format(record)
assert "Debug message" in result
assert "DEBUG" in result
def test_format_info_level(self):
"""Test formatting INFO level message"""
formatter = CustomConsoleFormatter('[%(levelname)s] %(message)s')
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Info message",
args=(),
exc_info=None
)
result = formatter.format(record)
assert "Info message" in result
assert "INFO" in result
def test_format_warning_level(self):
"""Test formatting WARNING level message"""
formatter = CustomConsoleFormatter('[%(levelname)s] %(message)s')
record = logging.LogRecord(
name="test",
level=logging.WARNING,
pathname="test.py",
lineno=1,
msg="Warning message",
args=(),
exc_info=None
)
result = formatter.format(record)
assert "Warning message" in result
assert "WARNING" in result
def test_format_error_level(self):
"""Test formatting ERROR level message"""
formatter = CustomConsoleFormatter('[%(levelname)s] %(message)s')
record = logging.LogRecord(
name="test",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error message",
args=(),
exc_info=None
)
result = formatter.format(record)
assert "Error message" in result
assert "ERROR" in result
def test_format_critical_level(self):
"""Test formatting CRITICAL level message"""
formatter = CustomConsoleFormatter('[%(levelname)s] %(message)s')
record = logging.LogRecord(
name="test",
level=logging.CRITICAL,
pathname="test.py",
lineno=1,
msg="Critical message",
args=(),
exc_info=None
)
result = formatter.format(record)
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

@@ -0,0 +1,124 @@
"""
Unit tests for CustomHandlerFilter in logging handling
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
import logging
from pathlib import Path
import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
CustomHandlerFilter,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test CustomHandlerFilter
class TestCustomHandlerFilter:
"""Test cases for CustomHandlerFilter"""
def test_filter_exceptions_for_console(self):
"""Test filtering exception records for console handler"""
handler_filter = CustomHandlerFilter('console', filter_exceptions=True)
record = logging.LogRecord(
name="test",
level=70, # EXCEPTION level
pathname="test.py",
lineno=1,
msg="Exception message",
args=(),
exc_info=None
)
record.levelname = "EXCEPTION"
result = handler_filter.filter(record)
assert result is False
def test_filter_non_exceptions_for_console(self):
"""Test non-exception records pass through console filter"""
handler_filter = CustomHandlerFilter('console', filter_exceptions=True)
record = logging.LogRecord(
name="test",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error message",
args=(),
exc_info=None
)
result = handler_filter.filter(record)
assert result is True
def test_filter_console_flag_for_file(self):
"""Test filtering console-flagged records for file handler"""
handler_filter = CustomHandlerFilter('file', filter_exceptions=False)
record = logging.LogRecord(
name="test",
level=logging.ERROR,
pathname="test.py",
lineno=1,
msg="Error message",
args=(),
exc_info=None
)
record.console = True
result = handler_filter.filter(record)
assert result is False
def test_filter_normal_record_for_file(self):
"""Test normal records pass through file filter"""
handler_filter = CustomHandlerFilter('file', filter_exceptions=False)
record = logging.LogRecord(
name="test",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Info message",
args=(),
exc_info=None
)
result = handler_filter.filter(record)
assert result is True
# __END__

View File

@@ -0,0 +1,209 @@
"""
Unit tests for Log handler management
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
import logging
from pathlib import Path
import pytest
from corelibs.logging_handling.log import (
Log,
LogParent,
LogSettings,
ConsoleFormatSettings,
ConsoleFormat,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test Handler Management
class TestHandlerManagement:
"""Test cases for handler management"""
def test_add_handler_before_init(self, tmp_log_path: Path):
"""Test adding handler before logger initialization"""
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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,
}
custom_handler = logging.StreamHandler()
custom_handler.set_name("custom")
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings,
other_handlers={"custom": custom_handler}
)
assert "custom" in log.handlers
def test_add_handler_after_init_raises(self, log_instance: Log):
"""Test adding handler after initialization raises error"""
custom_handler = logging.StreamHandler()
custom_handler.set_name("custom2")
with pytest.raises(ValueError, match="Cannot add handler"):
log_instance.add_handler("custom2", custom_handler)
def test_add_duplicate_handler_returns_false(self):
"""Test adding duplicate handler returns False"""
# Create a Log instance in a way we can test before initialization
log = object.__new__(Log)
LogParent.__init__(log)
log.handlers = {}
log.listener = None
handler1 = logging.StreamHandler()
handler1.set_name("test")
handler2 = logging.StreamHandler()
handler2.set_name("test")
result1 = log.add_handler("test", handler1)
assert result1 is True
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

@@ -0,0 +1,94 @@
"""
Unit tests for Log, Logger, and LogParent classes
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
from pathlib import Path
import pytest
from corelibs.logging_handling.log import (
Log,
Logger,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test Logger Class
class TestLogger:
"""Test cases for Logger class"""
def test_logger_init(self, log_instance: Log):
"""Test Logger initialization"""
logger_settings = log_instance.get_logger_settings()
logger = Logger(logger_settings)
assert logger.logger is not None
assert logger.lg == logger.logger
assert logger.l == logger.logger
assert isinstance(logger.handlers, dict)
assert len(logger.handlers) > 0
def test_logger_logging_methods(self, log_instance: Log, tmp_log_path: Path):
"""Test Logger logging methods"""
logger_settings = log_instance.get_logger_settings()
logger = Logger(logger_settings)
logger.debug("Debug from Logger")
logger.info("Info from Logger")
logger.warning("Warning from Logger")
logger.error("Error from Logger")
logger.critical("Critical from Logger")
log_file = tmp_log_path / "testlog.log"
content = log_file.read_text()
assert "Debug from Logger" in content
assert "Info from Logger" in content
assert "Warning from Logger" in content
assert "Error from Logger" in content
assert "Critical from Logger" in content
def test_logger_shared_queue(self, log_instance: Log):
"""Test Logger shares the same log queue"""
logger_settings = log_instance.get_logger_settings()
logger = Logger(logger_settings)
assert logger.log_queue == log_instance.log_queue
# __END__

View File

@@ -0,0 +1,116 @@
"""
Unit tests for Log, Logger, and LogParent classes
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
import logging
from pathlib import Path
import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test Edge Cases
class TestEdgeCases:
"""Test edge cases and special scenarios"""
def test_log_name_sanitization(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test log name with special characters gets sanitized"""
_ = Log(
log_path=tmp_log_path,
log_name="test@#$%log",
log_settings=basic_log_settings
)
# Special characters should be removed from filename
log_file = tmp_log_path / "testlog.log"
assert log_file.exists() or any(tmp_log_path.glob("test*.log"))
def test_multiple_log_instances(self, tmp_log_path: Path, basic_log_settings: LogSettings):
"""Test creating multiple Log instances"""
log1 = Log(tmp_log_path, "log1", basic_log_settings)
log2 = Log(tmp_log_path, "log2", basic_log_settings)
log1.info("From log1")
log2.info("From log2")
log_file1 = tmp_log_path / "log1.log"
log_file2 = tmp_log_path / "log2.log"
assert log_file1.exists()
assert log_file2.exists()
assert "From log1" in log_file1.read_text()
assert "From log2" in log_file2.read_text()
def test_destructor_calls_stop_listener(self, tmp_log_path: Path):
"""Test destructor calls stop_listener"""
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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,
}
log = Log(tmp_log_path, "test", settings)
del log
# Check that the log file was finalized
log_file = tmp_log_path / "test.log"
if log_file.exists():
content = log_file.read_text()
assert "[END]" in content
def test_get_logger_settings(self, log_instance: Log):
"""Test get_logger_settings returns correct structure"""
settings = log_instance.get_logger_settings()
assert "logger" in settings
assert "log_queue" in settings
assert isinstance(settings["logger"], logging.Logger)
# __END__

View File

@@ -0,0 +1,144 @@
"""
Unit tests for Log, Logger, and LogParent classes
"""
# pylint: disable=protected-access,redefined-outer-name,use-implicit-booleaness-not-comparison
import logging
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from multiprocessing import Queue
import pytest
from corelibs.logging_handling.log import (
Log,
LogSettings,
ConsoleFormatSettings,
)
from corelibs.logging_handling.logging_level_handling.logging_level import LoggingLevel
# MARK: Fixtures
@pytest.fixture
def tmp_log_path(tmp_path: Path) -> Path:
"""Create a temporary directory for log files"""
log_dir = tmp_path / "logs"
log_dir.mkdir(exist_ok=True)
return log_dir
@pytest.fixture
def basic_log_settings() -> LogSettings:
"""Basic log settings for testing"""
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,
}
@pytest.fixture
def log_instance(tmp_log_path: Path, basic_log_settings: LogSettings) -> Log:
"""Create a basic Log instance"""
return Log(
log_path=tmp_log_path,
log_name="test_log",
log_settings=basic_log_settings
)
# MARK: Test Queue Listener
class TestQueueListener:
"""Test cases for queue listener functionality"""
@patch('logging.handlers.QueueListener')
def test_init_listener(self, mock_listener_class: MagicMock, tmp_log_path: Path):
"""Test listener initialization with queue"""
# Create a mock queue without spec to allow attribute setting
mock_queue = MagicMock()
mock_queue.empty.return_value = True
# Configure queue attributes to prevent TypeError in comparisons
mock_queue._maxsize = -1 # Standard Queue default
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings
)
assert log.log_queue == mock_queue
mock_listener_class.assert_called_once()
def test_stop_listener_no_listener(self, log_instance: Log):
"""Test stop_listener when no listener exists"""
log_instance.stop_listener() # Should not raise
@patch('logging.handlers.QueueListener')
def test_stop_listener_with_listener(self, mock_listener_class: MagicMock, tmp_log_path: Path):
"""Test stop_listener with active listener"""
# Create a mock queue without spec to allow attribute setting
mock_queue = MagicMock()
mock_queue.empty.return_value = True
# Configure queue attributes to prevent TypeError in comparisons
mock_queue._maxsize = -1 # Standard Queue default
mock_listener = MagicMock()
mock_listener_class.return_value = mock_listener
settings: LogSettings = {
"log_level_console": LoggingLevel.WARNING,
"log_level_file": LoggingLevel.DEBUG,
"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
}
log = Log(
log_path=tmp_log_path,
log_name="test",
log_settings=settings
)
log.stop_listener()
mock_listener.stop.assert_called_once()
# MARK: Test Static Methods
class TestStaticMethods:
"""Test cases for static methods"""
@patch('logging.getLogger')
def test_init_worker_logging(self, mock_get_logger: MagicMock):
"""Test init_worker_logging static method"""
mock_queue = Mock(spec=Queue)
mock_logger = MagicMock()
mock_get_logger.return_value = mock_logger
result = Log.init_worker_logging(mock_queue)
assert result == mock_logger
mock_get_logger.assert_called_once_with()
mock_logger.setLevel.assert_called_once_with(logging.DEBUG)
mock_logger.handlers.clear.assert_called_once()
assert mock_logger.addHandler.called
# __END__

View File

@@ -0,0 +1,503 @@
"""
Test cases for ErrorMessage class
"""
# pylint: disable=use-implicit-booleaness-not-comparison
from typing import Any
import pytest
from corelibs.logging_handling.error_handling import ErrorMessage
class TestErrorMessageWarnings:
"""Test cases for warning-related methods"""
def test_add_warning_basic(self):
"""Test adding a basic warning message"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
message = {"code": "W001", "description": "Test warning"}
error_msg.add_warning(message)
warnings = error_msg.get_warnings()
assert len(warnings) == 1
assert warnings[0]["code"] == "W001"
assert warnings[0]["description"] == "Test warning"
assert warnings[0]["level"] == "Warning"
def test_add_warning_with_base_message(self):
"""Test adding a warning with base message"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
base_message = {"timestamp": "2025-10-24", "module": "test"}
message = {"code": "W002", "description": "Another warning"}
error_msg.add_warning(message, base_message)
warnings = error_msg.get_warnings()
assert len(warnings) == 1
assert warnings[0]["timestamp"] == "2025-10-24"
assert warnings[0]["module"] == "test"
assert warnings[0]["code"] == "W002"
assert warnings[0]["description"] == "Another warning"
assert warnings[0]["level"] == "Warning"
def test_add_warning_with_none_base_message(self):
"""Test adding a warning with None as base message"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
message = {"code": "W003", "description": "Warning with None base"}
error_msg.add_warning(message, None)
warnings = error_msg.get_warnings()
assert len(warnings) == 1
assert warnings[0]["code"] == "W003"
assert warnings[0]["level"] == "Warning"
def test_add_warning_with_invalid_base_message(self):
"""Test adding a warning with invalid base message (not a dict)"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
message = {"code": "W004", "description": "Warning with invalid base"}
error_msg.add_warning(message, "invalid_base") # type: ignore
warnings = error_msg.get_warnings()
assert len(warnings) == 1
assert warnings[0]["code"] == "W004"
assert warnings[0]["level"] == "Warning"
def test_add_multiple_warnings(self):
"""Test adding multiple warnings"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
error_msg.add_warning({"code": "W001", "description": "First warning"})
error_msg.add_warning({"code": "W002", "description": "Second warning"})
error_msg.add_warning({"code": "W003", "description": "Third warning"})
warnings = error_msg.get_warnings()
assert len(warnings) == 3
assert warnings[0]["code"] == "W001"
assert warnings[1]["code"] == "W002"
assert warnings[2]["code"] == "W003"
def test_get_warnings_empty(self):
"""Test getting warnings when list is empty"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
warnings = error_msg.get_warnings()
assert warnings == []
assert len(warnings) == 0
def test_has_warnings_true(self):
"""Test has_warnings returns True when warnings exist"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
error_msg.add_warning({"code": "W001", "description": "Test warning"})
assert error_msg.has_warnings() is True
def test_has_warnings_false(self):
"""Test has_warnings returns False when no warnings exist"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
assert error_msg.has_warnings() is False
def test_reset_warnings(self):
"""Test resetting warnings list"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
error_msg.add_warning({"code": "W001", "description": "Test warning"})
assert error_msg.has_warnings() is True
error_msg.reset_warnings()
assert error_msg.has_warnings() is False
assert len(error_msg.get_warnings()) == 0
def test_warning_level_override(self):
"""Test that level is always set to Warning even if base contains different level"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
base_message = {"level": "Error"} # Should be overridden
message = {"code": "W001", "description": "Test warning"}
error_msg.add_warning(message, base_message)
warnings = error_msg.get_warnings()
assert warnings[0]["level"] == "Warning"
class TestErrorMessageErrors:
"""Test cases for error-related methods"""
def test_add_error_basic(self):
"""Test adding a basic error message"""
error_msg = ErrorMessage()
error_msg.reset_errors()
message = {"code": "E001", "description": "Test error"}
error_msg.add_error(message)
errors = error_msg.get_errors()
assert len(errors) == 1
assert errors[0]["code"] == "E001"
assert errors[0]["description"] == "Test error"
assert errors[0]["level"] == "Error"
def test_add_error_with_base_message(self):
"""Test adding an error with base message"""
error_msg = ErrorMessage()
error_msg.reset_errors()
base_message = {"timestamp": "2025-10-24", "module": "test"}
message = {"code": "E002", "description": "Another error"}
error_msg.add_error(message, base_message)
errors = error_msg.get_errors()
assert len(errors) == 1
assert errors[0]["timestamp"] == "2025-10-24"
assert errors[0]["module"] == "test"
assert errors[0]["code"] == "E002"
assert errors[0]["description"] == "Another error"
assert errors[0]["level"] == "Error"
def test_add_error_with_none_base_message(self):
"""Test adding an error with None as base message"""
error_msg = ErrorMessage()
error_msg.reset_errors()
message = {"code": "E003", "description": "Error with None base"}
error_msg.add_error(message, None)
errors = error_msg.get_errors()
assert len(errors) == 1
assert errors[0]["code"] == "E003"
assert errors[0]["level"] == "Error"
def test_add_error_with_invalid_base_message(self):
"""Test adding an error with invalid base message (not a dict)"""
error_msg = ErrorMessage()
error_msg.reset_errors()
message = {"code": "E004", "description": "Error with invalid base"}
error_msg.add_error(message, "invalid_base") # type: ignore
errors = error_msg.get_errors()
assert len(errors) == 1
assert errors[0]["code"] == "E004"
assert errors[0]["level"] == "Error"
def test_add_multiple_errors(self):
"""Test adding multiple errors"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.add_error({"code": "E001", "description": "First error"})
error_msg.add_error({"code": "E002", "description": "Second error"})
error_msg.add_error({"code": "E003", "description": "Third error"})
errors = error_msg.get_errors()
assert len(errors) == 3
assert errors[0]["code"] == "E001"
assert errors[1]["code"] == "E002"
assert errors[2]["code"] == "E003"
def test_get_errors_empty(self):
"""Test getting errors when list is empty"""
error_msg = ErrorMessage()
error_msg.reset_errors()
errors = error_msg.get_errors()
assert errors == []
assert len(errors) == 0
def test_has_errors_true(self):
"""Test has_errors returns True when errors exist"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.add_error({"code": "E001", "description": "Test error"})
assert error_msg.has_errors() is True
def test_has_errors_false(self):
"""Test has_errors returns False when no errors exist"""
error_msg = ErrorMessage()
error_msg.reset_errors()
assert error_msg.has_errors() is False
def test_reset_errors(self):
"""Test resetting errors list"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.add_error({"code": "E001", "description": "Test error"})
assert error_msg.has_errors() is True
error_msg.reset_errors()
assert error_msg.has_errors() is False
assert len(error_msg.get_errors()) == 0
def test_error_level_override(self):
"""Test that level is always set to Error even if base contains different level"""
error_msg = ErrorMessage()
error_msg.reset_errors()
base_message = {"level": "Warning"} # Should be overridden
message = {"code": "E001", "description": "Test error"}
error_msg.add_error(message, base_message)
errors = error_msg.get_errors()
assert errors[0]["level"] == "Error"
class TestErrorMessageMixed:
"""Test cases for mixed warning and error operations"""
def test_errors_and_warnings_independent(self):
"""Test that errors and warnings are stored independently"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.reset_warnings()
error_msg.add_error({"code": "E001", "description": "Test error"})
error_msg.add_warning({"code": "W001", "description": "Test warning"})
assert len(error_msg.get_errors()) == 1
assert len(error_msg.get_warnings()) == 1
assert error_msg.has_errors() is True
assert error_msg.has_warnings() is True
def test_reset_errors_does_not_affect_warnings(self):
"""Test that resetting errors does not affect warnings"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.reset_warnings()
error_msg.add_error({"code": "E001", "description": "Test error"})
error_msg.add_warning({"code": "W001", "description": "Test warning"})
error_msg.reset_errors()
assert error_msg.has_errors() is False
assert error_msg.has_warnings() is True
assert len(error_msg.get_warnings()) == 1
def test_reset_warnings_does_not_affect_errors(self):
"""Test that resetting warnings does not affect errors"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.reset_warnings()
error_msg.add_error({"code": "E001", "description": "Test error"})
error_msg.add_warning({"code": "W001", "description": "Test warning"})
error_msg.reset_warnings()
assert error_msg.has_errors() is True
assert error_msg.has_warnings() is False
assert len(error_msg.get_errors()) == 1
class TestErrorMessageClassVariables:
"""Test cases to verify class-level variable behavior"""
def test_class_variable_shared_across_instances(self):
"""Test that error and warning lists are shared across instances"""
error_msg1 = ErrorMessage()
error_msg2 = ErrorMessage()
error_msg1.reset_errors()
error_msg1.reset_warnings()
error_msg1.add_error({"code": "E001", "description": "Error from instance 1"})
error_msg1.add_warning({"code": "W001", "description": "Warning from instance 1"})
# Both instances should see the same data
assert len(error_msg2.get_errors()) == 1
assert len(error_msg2.get_warnings()) == 1
assert error_msg2.has_errors() is True
assert error_msg2.has_warnings() is True
def test_reset_affects_all_instances(self):
"""Test that reset operations affect all instances"""
error_msg1 = ErrorMessage()
error_msg2 = ErrorMessage()
error_msg1.reset_errors()
error_msg1.reset_warnings()
error_msg1.add_error({"code": "E001", "description": "Test error"})
error_msg1.add_warning({"code": "W001", "description": "Test warning"})
error_msg2.reset_errors()
# Both instances should reflect the reset
assert error_msg1.has_errors() is False
assert error_msg2.has_errors() is False
error_msg2.reset_warnings()
assert error_msg1.has_warnings() is False
assert error_msg2.has_warnings() is False
class TestErrorMessageEdgeCases:
"""Test edge cases and special scenarios"""
def test_empty_message_dict(self):
"""Test adding empty message dictionaries"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.reset_warnings()
error_msg.add_error({})
error_msg.add_warning({})
errors = error_msg.get_errors()
warnings = error_msg.get_warnings()
assert len(errors) == 1
assert len(warnings) == 1
assert errors[0] == {"level": "Error"}
assert warnings[0] == {"level": "Warning"}
def test_message_with_complex_data(self):
"""Test adding messages with complex data structures"""
error_msg = ErrorMessage()
error_msg.reset_errors()
complex_message = {
"code": "E001",
"description": "Complex error",
"details": {
"nested": "data",
"list": [1, 2, 3],
},
"count": 42,
}
error_msg.add_error(complex_message)
errors = error_msg.get_errors()
assert errors[0]["code"] == "E001"
assert errors[0]["details"]["nested"] == "data"
assert errors[0]["details"]["list"] == [1, 2, 3]
assert errors[0]["count"] == 42
assert errors[0]["level"] == "Error"
def test_base_message_merge_override(self):
"""Test that message values override base_message values"""
error_msg = ErrorMessage()
error_msg.reset_errors()
base_message = {"code": "BASE", "description": "Base description", "timestamp": "2025-10-24"}
message = {"code": "E001", "description": "Override description"}
error_msg.add_error(message, base_message)
errors = error_msg.get_errors()
assert errors[0]["code"] == "E001" # Overridden
assert errors[0]["description"] == "Override description" # Overridden
assert errors[0]["timestamp"] == "2025-10-24" # From base
assert errors[0]["level"] == "Error" # Set by add_error
def test_sequential_operations(self):
"""Test sequential add and reset operations"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.add_error({"code": "E001"})
assert len(error_msg.get_errors()) == 1
error_msg.add_error({"code": "E002"})
assert len(error_msg.get_errors()) == 2
error_msg.reset_errors()
assert len(error_msg.get_errors()) == 0
error_msg.add_error({"code": "E003"})
assert len(error_msg.get_errors()) == 1
assert error_msg.get_errors()[0]["code"] == "E003"
class TestParametrized:
"""Parametrized tests for comprehensive coverage"""
@pytest.mark.parametrize("base_message,message,expected_keys", [
(None, {"code": "E001"}, {"code", "level"}),
({}, {"code": "E001"}, {"code", "level"}),
({"timestamp": "2025-10-24"}, {"code": "E001"}, {"code", "level", "timestamp"}),
({"a": 1, "b": 2}, {"c": 3}, {"a", "b", "c", "level"}),
])
def test_error_message_merge_parametrized(
self,
base_message: dict[str, Any] | None,
message: dict[str, Any],
expected_keys: set[str]
):
"""Test error message merging with various combinations"""
error_msg = ErrorMessage()
error_msg.reset_errors()
error_msg.add_error(message, base_message)
errors = error_msg.get_errors()
assert len(errors) == 1
assert set(errors[0].keys()) == expected_keys
assert errors[0]["level"] == "Error"
@pytest.mark.parametrize("base_message,message,expected_keys", [
(None, {"code": "W001"}, {"code", "level"}),
({}, {"code": "W001"}, {"code", "level"}),
({"timestamp": "2025-10-24"}, {"code": "W001"}, {"code", "level", "timestamp"}),
({"a": 1, "b": 2}, {"c": 3}, {"a", "b", "c", "level"}),
])
def test_warning_message_merge_parametrized(
self,
base_message: dict[str, Any] | None,
message: dict[str, Any],
expected_keys: set[str]
):
"""Test warning message merging with various combinations"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
error_msg.add_warning(message, base_message)
warnings = error_msg.get_warnings()
assert len(warnings) == 1
assert set(warnings[0].keys()) == expected_keys
assert warnings[0]["level"] == "Warning"
@pytest.mark.parametrize("count", [0, 1, 5, 10, 100])
def test_multiple_errors_parametrized(self, count: int):
"""Test adding multiple errors"""
error_msg = ErrorMessage()
error_msg.reset_errors()
for i in range(count):
error_msg.add_error({"code": f"E{i:03d}"})
errors = error_msg.get_errors()
assert len(errors) == count
assert error_msg.has_errors() == (count > 0)
@pytest.mark.parametrize("count", [0, 1, 5, 10, 100])
def test_multiple_warnings_parametrized(self, count: int):
"""Test adding multiple warnings"""
error_msg = ErrorMessage()
error_msg.reset_warnings()
for i in range(count):
error_msg.add_warning({"code": f"W{i:03d}"})
warnings = error_msg.get_warnings()
assert len(warnings) == count
assert error_msg.has_warnings() == (count > 0)
# __END__

View File

@@ -1,239 +0,0 @@
"""
PyTest: string_handling/string_helpers
"""
from textwrap import shorten
import pytest
from corelibs.string_handling.string_helpers import (
shorten_string, left_fill, format_number
)
class TestShortenString:
"""Tests for shorten_string function"""
def test_string_shorter_than_length(self):
"""Test that strings shorter than length are returned unchanged"""
result = shorten_string("hello", 10)
assert result == "hello"
def test_string_equal_to_length(self):
"""Test that strings equal to length are returned unchanged"""
result = shorten_string("hello", 5)
assert result == "hello"
def test_hard_shorten_true(self):
"""Test hard shortening with default placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True)
assert result == "hell [~]"
def test_hard_shorten_custom_placeholder(self):
"""Test hard shortening with custom placeholder"""
result = shorten_string("hello world", 8, hard_shorten=True, placeholder="...")
assert result == "hello..."
def test_no_spaces_auto_hard_shorten(self):
"""Test that strings without spaces automatically use hard shorten"""
result = shorten_string("helloworld", 8)
assert result == "hell [~]"
def test_soft_shorten_with_spaces(self):
"""Test soft shortening using textwrap.shorten"""
result = shorten_string("hello world test", 12)
# Should use textwrap.shorten behavior
expected = shorten("hello world test", width=12, placeholder=" [~]")
assert result == expected
def test_placeholder_too_large_hard_shorten(self):
"""Test error when placeholder is larger than allowed length"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, hard_shorten=True, placeholder=" [~]")
def test_placeholder_too_large_no_spaces(self):
"""Test error when placeholder is larger than allowed length for string without spaces"""
with pytest.raises(ValueError, match="Cannot shorten string: placeholder .* is too large for max width"):
shorten_string("hello", 3, placeholder=" [~]")
def test_textwrap_shorten_error(self):
"""Test handling of textwrap.shorten ValueError"""
# This might be tricky to trigger, but we can mock it
with pytest.raises(ValueError, match="Cannot shorten string:"):
# Very short length that might cause textwrap.shorten to fail
shorten_string("hello world", 1, hard_shorten=False)
def test_type_conversion(self):
"""Test that inputs are converted to proper types"""
result = shorten_string(12345, 8, hard_shorten=True)
assert result == "12345"
def test_empty_string(self):
"""Test with empty string"""
result = shorten_string("", 5)
assert result == ""
def test_zero_length(self):
"""Test with zero length"""
with pytest.raises(ValueError):
shorten_string("hello", 0, hard_shorten=True)
class TestLeftFill:
"""Tests for left_fill function"""
def test_basic_left_fill(self):
"""Test basic left filling with spaces"""
result = left_fill("hello", 10)
assert result == " hello"
assert len(result) == 10
def test_custom_fill_character(self):
"""Test left filling with custom character"""
result = left_fill("hello", 10, "0")
assert result == "00000hello"
def test_string_longer_than_width(self):
"""Test when string is longer than width"""
result = left_fill("hello world", 5)
assert result == "hello world" # Should return original string
def test_string_equal_to_width(self):
"""Test when string equals width"""
result = left_fill("hello", 5)
assert result == "hello"
def test_negative_width(self):
"""Test with negative width"""
result = left_fill("hello", -5)
assert result == "hello" # Should use string length
def test_zero_width(self):
"""Test with zero width"""
result = left_fill("hello", 0)
assert result == "hello" # Should return original string
def test_invalid_fill_character(self):
"""Test with invalid fill character (not single char)"""
result = left_fill("hello", 10, "abc")
assert result == " hello" # Should default to space
def test_empty_fill_character(self):
"""Test with empty fill character"""
result = left_fill("hello", 10, "")
assert result == " hello" # Should default to space
def test_empty_string(self):
"""Test with empty string"""
result = left_fill("", 5)
assert result == " "
class TestFormatNumber:
"""Tests for format_number function"""
def test_integer_default_precision(self):
"""Test formatting integer with default precision"""
result = format_number(1234)
assert result == "1,234"
def test_float_default_precision(self):
"""Test formatting float with default precision"""
result = format_number(1234.56)
assert result == "1,235" # Should round to nearest integer
def test_with_precision(self):
"""Test formatting with specified precision"""
result = format_number(1234.5678, 2)
assert result == "1,234.57"
def test_large_number(self):
"""Test formatting large number"""
result = format_number(1234567.89, 2)
assert result == "1,234,567.89"
def test_zero(self):
"""Test formatting zero"""
result = format_number(0)
assert result == "0"
def test_negative_number(self):
"""Test formatting negative number"""
result = format_number(-1234.56, 2)
assert result == "-1,234.56"
def test_negative_precision(self):
"""Test with negative precision (should default to 0)"""
result = format_number(1234.56, -1)
assert result == "1,235"
def test_excessive_precision(self):
"""Test with precision > 100 (should default to 0)"""
result = format_number(1234.56, 101)
assert result == "1,235"
def test_precision_boundary_values(self):
"""Test precision boundary values"""
# Test precision = 0 (should work)
result = format_number(1234.56, 0)
assert result == "1,235"
# Test precision = 100 (should work)
result = format_number(1234.56, 100)
assert "1,234.56" in result # Will have many trailing zeros
def test_small_decimal(self):
"""Test formatting small decimal number"""
result = format_number(0.123456, 4)
assert result == "0.1235"
def test_very_small_number(self):
"""Test formatting very small number"""
result = format_number(0.001, 3)
assert result == "0.001"
# Additional integration tests
class TestIntegration:
"""Integration tests combining functions"""
def test_format_and_fill(self):
"""Test formatting a number then left filling"""
formatted = format_number(1234.56, 2)
result = left_fill(formatted, 15)
assert result.endswith("1,234.56")
assert len(result) == 15
def test_format_and_shorten(self):
"""Test formatting a large number then shortening"""
formatted = format_number(123456789.123, 3)
result = shorten_string(formatted, 10)
assert len(result) <= 10
# Fixtures for parameterized tests
@pytest.mark.parametrize("input_str,length,expected", [
("hello", 10, "hello"),
("hello world", 5, "h [~]"),
("test", 4, "test"),
("", 5, ""),
])
def test_shorten_string_parametrized(input_str: str, length: int, expected: str):
"""Parametrized test for shorten_string"""
result = shorten_string(input_str, length, hard_shorten=True)
if expected.endswith(" [~]"):
assert result.endswith(" [~]")
assert len(result) == length
else:
assert result == expected
@pytest.mark.parametrize("number,precision,expected", [
(1000, 0, "1,000"),
(1234.56, 2, "1,234.56"),
(0, 1, "0.0"),
(-500, 0, "-500"),
])
def test_format_number_parametrized(number: float | int, precision: int, expected: str):
"""Parametrized test for format_number"""
assert format_number(number, precision) == expected
# __END__

View File

@@ -1,157 +0,0 @@
"""
PyTest: string_handling/timestamp_strings
"""
from datetime import datetime
from unittest.mock import patch, MagicMock
from zoneinfo import ZoneInfo
import pytest
# Assuming the class is in a file called timestamp_strings.py
from corelibs.string_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.string_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.string_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.string_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.string_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.string_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.string_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.string_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
@patch('corelibs.string_handling.timestamp_strings.ZoneInfo')
def test_zoneinfo_called_correctly(self, mock_zoneinfo: MagicMock):
"""Test that ZoneInfo is called with correct timezone"""
with patch('corelibs.string_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_edge_case_midnight(self):
"""Test timestamp formatting at midnight"""
with patch('corelibs.string_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.string_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

@@ -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)

Some files were not shown because too many files have changed in this diff Show More