API reference¶
datetime utilities¶
- ensure_tzinfo(val: datetime, tz_or_offset: str | int | timedelta | tzinfo | ZoneInfo | timezone = 'UTC', *, is_dst: bool = False) datetime¶
Creates timezone aware datetime object for
val.if
valis naive datetime, new value will be created as datetime localized intz_or_offsettimezoneif
valis already timezone aware, it will be converted totz_or_offsettimezone using datetime.datetime.astimezone
- Parameters:
val – Input value for conversion
tz_or_offset – Anything that timezone_or_offset accepts
is_dst – used to determine the correct timezone in the ambiguous period at the end of daylight saving time. Use
is_dst=Noneto raise an AmbiguousTimeError for ambiguous times at the end of daylight saving time.
- Returns:
Timezone aware datetime object
- Raises:
ValueError – When timezone of offset can’t be parsed / determined from
tz_or_offset
Note
This tries to provide safe(ish) implementation for handling naive datetime objects, but ultimate solution is to not use naive datetime objects ever/anywhere. Recommendation is to go with
pip install pendulumand leave this crap behind to history.
- iter_year_month(start: date | datetime, end: date | None = None, *, include_start: bool = False, include_end: bool = False)¶
Generates range of date(year, month, 1) from
starttoend.when
start > endgenerated range is emptywhen
start == endgenerated range may contain singledateobject depending oninclude_startorinclude_end
- Parameters:
start – begin of range
end – end of range. If
None, assumes,start == endinclude_start – include
startin generated rangeinclude_end – include
endin generated range
- next_working_day(from_: date | None = None, holidays_calendar={})¶
Finds next work day from from_ or today.
- timezone_or_offset(from_: str | int | timedelta | tzinfo | ZoneInfo | timezone | None) ZoneInfo | timezone¶
Given
from_creates datetime.timezone or zoneinfo.ZoneInfo as result.from_can be any of following:str (ie. “-02:42”, None, “”, “Z”, …) which is ISO8601 offset
str (ie. “Europe/Zagreb”) which is timezone name
int (ie. -9000) which is total number of seconds in time offset
datetime.timedelta
datetime.tzinfo or something that behaves like it
benchmarking utilities¶
file utilities¶
- abspath_if_relative(relative_path: str | Path, relative_to: str | Path)¶
Creates absolute path from relative, but places it under other path.
Example
>>> abspath_if_relative('foo/bar/baz', relative_to='/tmp') '/tmp/foo/bar/baz'
- file_checksum(file_path: str | Path, hashlib_callable)¶
Given path of the file and hash function, calculates file digest
- move_and_create_dest(src_path: str | Path, dst_dir: str | Path)¶
Moves
src_pathtodst_dirdirectory.Expects
dst_dirto be directory and if it doesn’t exits, tries to create it.
logging utilities¶
- class PrettyFormatter(fmt: str | None = None, datefmt: str | None = None, style: logging._FormatStyle = '%', validate: bool = True, *, defaults: Mapping[str, Any] | None = None, force_single_line: bool = True, colorize: bool = False, log_colors: LogColors | None = None, secondary_log_colors: SecondaryLogColors | None = None, reset: bool = True, stream: IO | None = None)¶
Logging formatter for pretty logs:
can optionally colorize output
can optionally force output to be single line
reformats logged tracebacks
To colorize output:
instantiate formatter with colorize = True
use color placeholders in format string, ie %(red)s text that will be red %(reset)s
Example YAML logging config to use it:
--- version: 1 loggers: my_app: handlers: - console level: INFO propagate: false handlers: console: class: logging.StreamHandler filters: - request_id stream: ext://sys.stdout formatter: colored_multiline formatters: colored_multiline: (): seveno_pyutil.PrettyFormatter force_single_line: false colorize: true format: >- lvl=%(log_color)s%(levelname)s%(reset)s ts=%(thin_white)s%(asctime)s%(reset)s msg=%(message)s colorless_single_line: (): seveno_pyutil.PrettyFormatter force_single_line: true colorize: false format: >- lvl=%(levelname)s ts=%(asctime)s msg=%(message)s
Some of available colors are:
[ 'black', 'bold_black', 'thin_black', 'red', 'bold_red', 'thin_red', 'green', 'bold_green', 'thin_green', 'yellow', 'bold_yellow', 'thin_yellow', 'blue', 'bold_blue', 'thin_blue', 'purple', 'bold_purple', 'thin_purple', 'cyan', 'bold_cyan', 'thin_cyan', 'white', 'bold_white', 'thin_white', "..." ]
Others can be retrieved via:
colorlog.ColoredFormatter()._escape_code_map("DEBUG").keys()
or check docs for colorlog.
- format(record: LogRecord) str¶
Format the specified record as text.
The record’s attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message.
- formatException(ei) str¶
Format and return the specified exception information as a string.
This default implementation just uses traceback.print_exception()
- class SQLFilter(*args, colorize_queries=False, multiline_queries=False, shorten_logs=True, **kwargs)¶
Filter for SQLAlchemy SQL loggers. Optionally reformats and colorizes queries.
To use it with Flask and SQLAlchemy:
configure Flask app
configure SQLAlchemy engine events (call :meth:register_sqlalchemy_logging_events)
configure logging
All three steps are presented in example below. After it has been configured, provides following logging placeholders:
placeholder
description
%(sql)s
Formatted SQL statement that was executed
%(sql_duration)s
Formatted duration of SQL execution
- Parameters:
colorize_queries (bool) – Should apply shell coloring escape sequences to formatted SQL?
multiline_queries (bool) – Should emit SQL as indented, multiline of single line log statements? In development it is usually nice to have it be True. In production environments, multiline logs are pain and should be avoided.
Example:
import logging from logging.config import dictConfig import flask from seveno_pyutil import FlaskSQLStats, SQLFilter dictConfig( { "version": 1, "disable_existing_loggers": False, "formatters": { "app": {"format": "lvl=%(levelname)s msg=%(message)s"}, "app.db": { "format": "lvl=%(levelname)s, sqld=%(sql_duration)s sql=%(sql)s msg=%(message)s" }, }, "filters": { "colored_sql": { "()": "seveno_pyutil.SQLFilter", "colorize_queries": True, "multiline_queries": True, } }, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "app", "stream": "ext://sys.stdout", }, "console_sqlalchemy": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "db", "filters": ["colored_sql"], "stream": "ext://sys.stdout", }, }, "loggers": { "myapp": { "level": "INFO", "propagate": False, "handlers": ["console"], }, "myapp.db": { "level": "DEBUG", "propagate": False, "handlers": ["console_sqlalchemy"], }, }, } ) class RequestLoggingMiddleware: def __init__(self, sql_logger_name, app=None): self.sql_logger_name = sql_logger_name if app: self.init_app(app) def init_app(self, app: flask.Flask): SQLFilter.register_sqlalchemy_logging_events(self.sql_logger_name) @app.before_request def log_current_request(): FlaskSQLStats.open() @flask.after_this_request def log_current_response(response): logger.info("Some HTTP request was processed :)") FlaskSQLStats.close() return response logger = logging.getLogger(__name__) app = flask.Flask(__name__) RequestLoggingMiddleware("myapp.db").init_app(app)
- filter(record: LogRecord)¶
Determine if the specified record is to be logged.
Returns True if the record should be logged, or False otherwise. If deemed appropriate, the record may be modified in-place.
- class StandardMetadataFilter(name='')¶
Filter that adds few more attributes to log records.
placeholder
description
%(hostname)s
hostname
%(isotime)s
Local time represented as ISO8601
%(isotime_utc)s
local time converted to UTC and represented as ISO8601 string
- filter(record)¶
Determine if the specified record is to be logged.
Returns True if the record should be logged, or False otherwise. If deemed appropriate, the record may be modified in-place.
- log_to_console_for(logger_name: str)¶
Sometimes, usually during development, we want to quickly see output of some particular package logger. This method will configure such logger to spit stuff out to console.
- log_to_tmp_file_for(logger_name: str, file_path: str | Path = '/tmp/seveno_pyutil.log')¶
Quick setup for given logger directing it to
/tmp/seveno_pyutil.logThis is of course mainly used during development, especially when playing with things in Python console.
- silence_logger(logger: Logger)¶
For given logger, replaces all its handlers with
logging.NullHandler.
metaprogramming helpers¶
- class LeafSubclassRetriever(base_class)¶
Bases:
objecthttp://code.activestate.com/recipes/577858-concrete-class-finder/
- value()¶
- all_subclasses(klass)¶
- getval(src: Mapping | object, attr: object, default=None)¶
Companion of dict.get and getattr which ensures default value even when original method would had returned None
Example:
d1 = {} d2 = {"foo": None} d3 = {"foo": "bar"} d1.get("foo", 42) # => 42 d2.get("foo", 42) # => None d3.get("foo", 42) # => "bar" getval(d1, "foo", 42) # => 42 getval(d2, "foo", 42) # => 42 getval(d3, "foo", 42) # => "bar" # and also getval({"a": ""}, "a", None) is None # => True # and with objects other than dicts class Foo: def __init__(self, foo=None): self.foo = foo class Bar: pass o1 = Bar() o2 = Foo() o3 = Foo("bar") getattr(o1, "foo", 42) # => 42 getattr(o2, "foo", 42) # => None getattr(o3, "foo", 42) # => "bar" getval(o1, "foo", 42) # => 42 getval(o2, "foo", 42) # => 42 getval(o3, "foo", 42) # => "bar"
- import_string(dotted_path)¶
Import a dotted module path and return the attribute/class designated by the last name in the path. Raise ImportError if the import failed.
- leaf_subclasses(klass)¶
Returns all leaf subclasses of given
klass
os utilities¶
- current_user()¶
Queries OS for current user username.
- current_user_home()¶
Queries OS for path to current user home directory.
string utilities¶
- is_blank(obj: Any) bool¶
True if obj is empty string, None, string that contains only spaces and space like characters, or iterable that contains only these kinds of strings/objects
collections utilities¶
- in_batches(iterable: Iterable[T], of_size: int = 1) Generator[Iterable[T]]¶
Generator that yields generator slices of iterable.
Since it is elegant and working flawlessly, it is shameless C/P from https://stackoverflow.com/questions/8991506/iterate-an-iterator-by-chunks-of-n-in-python/8998040#8998040
Warning
Each returned batch should be completely consumed before next batch is yielded. See example below to better understand what that means.
Example:
from seveno_pyutil import in_batches g = (o for o in range(10)) for batch in in_batches(g, of_size=3): print(list(batch)) # [0, 1, 2] # [3, 4, 5] # [6, 7, 8] # [9] # And this happens if whole batch is not consumed before yielding another one... g = list(range(10)) for batch in in_batches(g, of_size=3): print( [next(batch), next(batch)] ) # [0, 1] # [2, 3] # [4, 5] # [6, 7] # [8, 9]
error utilities¶
- class ExceptionsAsErrors(errors_store: Mapping | object, subkey=None)¶
Bases:
objectContext manager that swallows exceptions and stores them as structured error dict that can later be added to marshmallow.ValidationError.
It uses add_error_to to update provided
errors_storewith caught exceptions.Example:
errors = {} with ExceptionsAsErrors(errors) as e: raise RuntimeError("ZOMG!") errors == {'_schema': ['ZOMG!']} errors = {} with ExceptionsAsErrors(errors, subkey="some_name") as e: raise RuntimeError("ZOMG!") errors == {'some_name': ['ZOMG!']}
- add_error_to(errors_store: Mapping | object, error: str | Sequence | object | Mapping | Exception)¶
Updates error store, merging messages from
errorExample:
errors_store = { "person": { "email": ["is not an email"], "name": ["is too long"], "date_of_birth": ["is not a date"] }, "job": ["is not from allowed values list"] } add_error_to(errors_store, {"person": {"email": "is from illegal domain"}}) errors_store == { "person": { "email": ["is not an email", "is from illegal domain"], "name": ["is too long"], "date_of_birth": ["is not a date"] }, "job": ["is not from allowed values list"] }
But, trying to do this:
add_error_to(errors_store, {"person": "is illegally formed"})
will add/update
_schemakey becausepersonhas child keys and replacing them with["is illegally formed"]would loose that data. Thus,errors_storewill look like this now:errors_store == { "person": { "email": ["is not an email", "is from illegal domain"], "name": ["is too long"], "date_of_birth": ["is not a date"], "_schema": ["is illegally formed"] }, "job": ["is not from allowed values list"] }
Another example of this behavior:
add_error_to(errors_store, {"job": {"title": "can't be blank"}})
will result in:
errors_store == { "person": { "email": ["is not an email", "is from illegal domain"], "name": ["is too long"], "date_of_birth": ["is not a date"], "_schema": ["is illegally formed"] }, "job": { "_schema": ["is not from allowed values list"], "title": ["can't be blank"] } }
Also, method is smart enough to correctly update errors store with either one or a sequence of messages. So both of these are valid:
add_error_to(errors_store, {"job": {"title": "is overpaid"}}) add_error_to(errors_store, {"job": {"title": ["is forbiden", "doesn't exist"]}}) errors_store == { "person": { "email": ["is not an email", "is from illegal domain"], "name": ["is too long"], "date_of_birth": ["is not a date"], "_schema": ["is illegally formed"] }, "job": { "_schema": ["is not from allowed values list"], "title": [ "can't be blank", "is overpaid", "is forbiden", "doesn't exist" ] } }
- Parameters:
errors_store – either a dict into which errors will be added or an object. If object we expect it to have attribute
errorsand that these are a dict. If no such attribute exists on given object, we will attach our own.