Compare commits
6 Commits
main
...
log_sys_te
| Author | SHA1 | Date |
|---|---|---|
|
|
2e0ba81f98 | |
|
|
7ccd7aa227 | |
|
|
6d16c6347f | |
|
|
f8b24082b9 | |
|
|
1dc27c5161 | |
|
|
14aefa4b11 |
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# An "impure" template thx to `pyproject.nix`,
|
||||
# https://pyproject-nix.github.io/pyproject.nix/templates.html#impure
|
||||
# https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix
|
||||
{
|
||||
description = "An impure overlay using `uv` with Nix(OS)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ nixpkgs, ... }:
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# XXX NOTE XXX, for now we overlay specific pkgs via
|
||||
# a major-version-pinned-`cpython`
|
||||
cpython = "python313";
|
||||
venv_dir = "py313";
|
||||
pypkgs = pkgs."${cpython}Packages";
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
|
||||
packages = with pkgs; [
|
||||
# XXX, ensure sh completions activate!
|
||||
bashInteractive
|
||||
bash-completion
|
||||
|
||||
# on nixos, use pkg(s)
|
||||
ruff
|
||||
pypkgs.ruff
|
||||
|
||||
uv
|
||||
python313 # ?TODO^ how to set from `cpython` above?
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
# unmask to debug **this** dev-shell-hook
|
||||
# set -e
|
||||
|
||||
# link-in c++ stdlib for various AOT-ext-pkgs (numpy, etc.)
|
||||
LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
|
||||
|
||||
export LD_LIBRARY_PATH
|
||||
|
||||
# RUNTIME-SETTINGS
|
||||
# ------ uv ------
|
||||
# - always use the ./py313/ venv-subdir
|
||||
# - sync env with all extras
|
||||
export UV_PROJECT_ENVIRONMENT=${venv_dir}
|
||||
uv sync --dev --all-extras --no-group lint
|
||||
|
||||
# ------ TIPS ------
|
||||
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy)
|
||||
# run the `nix develop` cmd with,
|
||||
# >> nix develop -c uv run xonsh
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -53,22 +53,33 @@ dependencies = [
|
|||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
# test suite
|
||||
# TODO: maybe some of these layout choices?
|
||||
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
||||
"pytest>=8.3.5",
|
||||
"pexpect>=4.9.0,<5",
|
||||
{include-group = 'devx'},
|
||||
{include-group = 'testing'},
|
||||
{include-group = 'repl'},
|
||||
]
|
||||
devx = [
|
||||
# `tractor.devx` tooling
|
||||
"greenback>=1.2.1,<2",
|
||||
"stackscope>=0.2.2,<0.3",
|
||||
# ^ requires this?
|
||||
"typing-extensions>=4.14.1",
|
||||
|
||||
]
|
||||
testing = [
|
||||
# test suite
|
||||
# TODO: maybe some of these layout choices?
|
||||
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
||||
"pytest>=8.3.5",
|
||||
"pexpect>=4.9.0,<5",
|
||||
]
|
||||
repl = [
|
||||
"pyperclip>=1.9.0",
|
||||
"prompt-toolkit>=3.0.50",
|
||||
"xonsh>=0.19.2",
|
||||
"psutil>=7.0.0",
|
||||
]
|
||||
lint = [
|
||||
"ruff>=0.9.6"
|
||||
]
|
||||
# TODO, add these with sane versions; were originally in
|
||||
# `requirements-docs.txt`..
|
||||
# docs = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
'''
|
||||
`tractor.log`-wrapping unit tests.
|
||||
|
||||
'''
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
import tractor
|
||||
|
||||
|
||||
def test_root_pkg_not_duplicated_in_logger_name():
|
||||
'''
|
||||
When both `pkg_name` and `name` are passed and they have
|
||||
a common `<root_name>.< >` prefix, ensure that it is not
|
||||
duplicated in the child's `StackLevelAdapter.name: str`.
|
||||
|
||||
'''
|
||||
project_name: str = 'pylib'
|
||||
pkg_path: str = 'pylib.subpkg.mod'
|
||||
|
||||
proj_log = tractor.log.get_logger(
|
||||
pkg_name=project_name,
|
||||
mk_sublog=False,
|
||||
)
|
||||
|
||||
sublog = tractor.log.get_logger(
|
||||
pkg_name=project_name,
|
||||
name=pkg_path,
|
||||
)
|
||||
|
||||
assert proj_log is not sublog
|
||||
assert sublog.name.count(proj_log.name) == 1
|
||||
assert 'mod' not in sublog.name
|
||||
|
||||
|
||||
# ?TODO, move this into internal libs?
|
||||
# -[ ] we already use it in `modden.config._pymod` as well
|
||||
def load_module_from_path(
|
||||
path: Path,
|
||||
module_name: str|None = None,
|
||||
) -> ModuleType:
|
||||
'''
|
||||
Taken from SO,
|
||||
https://stackoverflow.com/a/67208147
|
||||
|
||||
which is based on stdlib docs,
|
||||
https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
||||
|
||||
'''
|
||||
module_name = module_name or path.stem
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
module_name,
|
||||
str(path),
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_implicit_mod_name_applied_for_child(
|
||||
testdir: pytest.Pytester,
|
||||
loglevel: str,
|
||||
):
|
||||
'''
|
||||
Verify that when `.log.get_logger(pkg_name='pylib')` is called
|
||||
from a given sub-mod from within the `pylib` pkg-path, we
|
||||
implicitly set the equiv of `name=__name__` from the caller's
|
||||
module.
|
||||
|
||||
'''
|
||||
# tractor.log.get_console_log(level=loglevel)
|
||||
proj_name: str = 'snakelib'
|
||||
mod_code: str = (
|
||||
f'import tractor\n'
|
||||
f'\n'
|
||||
f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n'
|
||||
)
|
||||
|
||||
# create a sub-module for each pkg layer
|
||||
_lib = testdir.mkpydir(proj_name)
|
||||
pkg: Path = Path(_lib)
|
||||
subpkg: Path = pkg / 'subpkg'
|
||||
subpkg.mkdir()
|
||||
|
||||
pkgmod: Path = subpkg / "__init__.py"
|
||||
pkgmod.touch()
|
||||
|
||||
_submod: Path = testdir.makepyfile(
|
||||
_mod=mod_code,
|
||||
)
|
||||
|
||||
pkg_mod = pkg / 'mod.py'
|
||||
pkg_subpkg_submod = subpkg / 'submod.py'
|
||||
shutil.copyfile(
|
||||
_submod,
|
||||
pkg_mod,
|
||||
)
|
||||
shutil.copyfile(
|
||||
_submod,
|
||||
pkg_subpkg_submod,
|
||||
)
|
||||
testdir.chdir()
|
||||
|
||||
# XXX NOTE, once the "top level" pkg mod has been
|
||||
# imported, we can then use `import` syntax to
|
||||
# import it's sub-pkgs and modules.
|
||||
pkgmod = load_module_from_path(
|
||||
Path(pkg / '__init__.py'),
|
||||
module_name=proj_name,
|
||||
)
|
||||
pkg_root_log = tractor.log.get_logger(
|
||||
pkg_name=proj_name,
|
||||
mk_sublog=False,
|
||||
)
|
||||
assert pkg_root_log.name == proj_name
|
||||
assert not pkg_root_log.logger.getChildren()
|
||||
|
||||
from snakelib import mod
|
||||
assert mod.log.name == proj_name
|
||||
|
||||
from snakelib.subpkg import submod
|
||||
assert (
|
||||
submod.log.name
|
||||
==
|
||||
submod.__package__ # ?TODO, use this in `.get_logger()` instead?
|
||||
==
|
||||
f'{proj_name}.subpkg'
|
||||
)
|
||||
|
||||
sub_logs = pkg_root_log.logger.getChildren()
|
||||
assert len(sub_logs) == 1 # only one nested sub-pkg module
|
||||
assert submod.log.logger in sub_logs
|
||||
|
||||
# breakpoint()
|
||||
|
||||
|
||||
# TODO, moar tests against existing feats:
|
||||
# ------ - ------
|
||||
# - [ ] color settings?
|
||||
# - [ ] header contents like,
|
||||
# - actor + thread + task names from various conc-primitives,
|
||||
# - [ ] `StackLevelAdapter` extensions,
|
||||
# - our custom levels/methods: `transport|runtime|cance|pdb|devx`
|
||||
# - [ ] custom-headers support?
|
||||
#
|
||||
|
||||
# TODO, test driven dev of new-ideas/long-wanted feats,
|
||||
# ------ - ------
|
||||
# - [ ] https://github.com/goodboy/tractor/issues/244
|
||||
# - [ ] @catern mentioned using a sync / deterministic sys
|
||||
# and in particular `svlogd`?
|
||||
# |_ https://smarden.org/runit/svlogd.8
|
||||
|
||||
# - [ ] using adapter vs. filters?
|
||||
# - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838
|
||||
|
||||
# - [ ] `.at_least_level()` optimization which short circuits wtv
|
||||
# `logging` is doing behind the scenes when the level filters
|
||||
# the emission..?
|
||||
|
||||
# - [ ] use of `.log.get_console_log()` in subactors and the
|
||||
# subtleties of ensuring it actually emits from a subproc.
|
||||
|
||||
# - [ ] this idea of activating per-subsys emissions with some
|
||||
# kind of `.name` filter passed to the runtime or maybe configured
|
||||
# via the root `StackLevelAdapter`?
|
||||
|
||||
# - [ ] use of `logging.dict.dictConfig()` to simplify the impl
|
||||
# of any of ^^ ??
|
||||
# - https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig
|
||||
# - https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
||||
# - https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
198
tractor/log.py
198
tractor/log.py
|
|
@ -14,11 +14,22 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Log like a forester!
|
||||
'''
|
||||
An enhanced logging subsys.
|
||||
|
||||
"""
|
||||
An extended logging layer using (for now) the stdlib's `logging`
|
||||
+ `colorlog` which embeds concurrency-primitive/runtime info into
|
||||
records (headers) to help you better grok your distributed systems
|
||||
built on `tractor`.
|
||||
|
||||
|
||||
'''
|
||||
from collections.abc import Mapping
|
||||
from inspect import (
|
||||
FrameInfo,
|
||||
getmodule,
|
||||
stack,
|
||||
)
|
||||
import sys
|
||||
import logging
|
||||
from logging import (
|
||||
|
|
@ -26,8 +37,10 @@ from logging import (
|
|||
Logger,
|
||||
StreamHandler,
|
||||
)
|
||||
import colorlog # type: ignore
|
||||
from types import ModuleType
|
||||
import warnings
|
||||
|
||||
import colorlog # type: ignore
|
||||
import trio
|
||||
|
||||
from ._state import current_actor
|
||||
|
|
@ -39,7 +52,7 @@ _default_loglevel: str = 'ERROR'
|
|||
# Super sexy formatting thanks to ``colorlog``.
|
||||
# (NOTE: we use the '{' format style)
|
||||
# Here, `thin_white` is just the layperson's gray.
|
||||
LOG_FORMAT = (
|
||||
LOG_FORMAT: str = (
|
||||
# "{bold_white}{log_color}{asctime}{reset}"
|
||||
"{log_color}{asctime}{reset}"
|
||||
" {bold_white}{thin_white}({reset}"
|
||||
|
|
@ -51,7 +64,7 @@ LOG_FORMAT = (
|
|||
" {reset}{bold_white}{thin_white}{message}"
|
||||
)
|
||||
|
||||
DATE_FORMAT = '%b %d %H:%M:%S'
|
||||
DATE_FORMAT: str = '%b %d %H:%M:%S'
|
||||
|
||||
# FYI, ERROR is 40
|
||||
# TODO: use a `bidict` to avoid the :155 check?
|
||||
|
|
@ -75,7 +88,10 @@ STD_PALETTE = {
|
|||
'TRANSPORT': 'cyan',
|
||||
}
|
||||
|
||||
BOLD_PALETTE = {
|
||||
BOLD_PALETTE: dict[
|
||||
str,
|
||||
dict[int, str],
|
||||
] = {
|
||||
'bold': {
|
||||
level: f"bold_{color}" for level, color in STD_PALETTE.items()}
|
||||
}
|
||||
|
|
@ -97,10 +113,17 @@ def at_least_level(
|
|||
return False
|
||||
|
||||
|
||||
# TODO: this isn't showing the correct '{filename}'
|
||||
# as it did before..
|
||||
# TODO, compare with using a "filter" instead?
|
||||
# - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838
|
||||
# |_corresponding dict-config,
|
||||
# https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig/7507842#7507842
|
||||
# - [ ] what's the benefit/tradeoffs?
|
||||
#
|
||||
class StackLevelAdapter(LoggerAdapter):
|
||||
'''
|
||||
A (software) stack oriented logger "adapter".
|
||||
|
||||
'''
|
||||
def at_least_level(
|
||||
self,
|
||||
level: str,
|
||||
|
|
@ -284,7 +307,9 @@ class ActorContextInfo(Mapping):
|
|||
|
||||
def get_logger(
|
||||
name: str|None = None,
|
||||
_root_name: str = _proj_name,
|
||||
pkg_name: str = _proj_name,
|
||||
# XXX, deprecated, use ^
|
||||
_root_name: str|None = None,
|
||||
|
||||
logger: Logger|None = None,
|
||||
|
||||
|
|
@ -293,22 +318,89 @@ def get_logger(
|
|||
# |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig
|
||||
# |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
||||
subsys_spec: str|None = None,
|
||||
mk_sublog: bool = True,
|
||||
|
||||
) -> StackLevelAdapter:
|
||||
'''
|
||||
Return the `tractor`-library root logger or a sub-logger for
|
||||
`name` if provided.
|
||||
|
||||
When `name` is left null we try to auto-detect the caller's
|
||||
`mod.__name__` and use that as a the sub-logger key.
|
||||
This allows for example creating a module level instance like,
|
||||
|
||||
.. code:: python
|
||||
|
||||
log = tractor.log.get_logger(_root_name='mylib')
|
||||
|
||||
and by default all console record headers will show the caller's
|
||||
(of any `log.<level>()`-method) correct sub-pkg's
|
||||
+ py-module-file.
|
||||
|
||||
'''
|
||||
if _root_name:
|
||||
msg: str = (
|
||||
'The `_root_name: str` param of `get_logger()` is now deprecated.\n'
|
||||
'Use the new `pkg_name: str` instead, it is the same usage.\n'
|
||||
)
|
||||
warnings.warn(
|
||||
msg,
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
pkg_name: str = _root_name or pkg_name
|
||||
log: Logger
|
||||
log = rlog = logger or logging.getLogger(_root_name)
|
||||
log = rlog = logger or logging.getLogger(pkg_name)
|
||||
|
||||
# Implicitly introspect the caller's module-name whenever `name`
|
||||
# if left as the null default.
|
||||
#
|
||||
# When the `pkg_name` is `in` in the `mod.__name__` we presume
|
||||
# this instance can be created as a sub-`StackLevelAdapter` and
|
||||
# that the intention is get free module-path tracing and
|
||||
# filtering (well once we implement that) oriented around the
|
||||
# py-module code hierarchy of the consuming project.
|
||||
if (
|
||||
pkg_name != _proj_name
|
||||
and
|
||||
name is None
|
||||
and
|
||||
mk_sublog
|
||||
):
|
||||
callstack: list[FrameInfo] = stack()
|
||||
caller_fi: FrameInfo = callstack[1]
|
||||
caller_mod: ModuleType = getmodule(caller_fi.frame)
|
||||
if caller_mod:
|
||||
# ?how is this `mod.__name__` defined?
|
||||
# -> well by how the mod is imported..
|
||||
# |_https://stackoverflow.com/a/15883682
|
||||
mod_name: str = caller_mod.__name__
|
||||
mod_pkg: str = caller_mod.__package__
|
||||
log.info(
|
||||
f'Generating sub-logger name,\n'
|
||||
f'{mod_pkg}.{mod_name}\n'
|
||||
)
|
||||
# if pkg_name in caller_mod.__package__:
|
||||
# from tractor.devx.debug import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
if (
|
||||
pkg_name
|
||||
# and
|
||||
# pkg_name in mod_name
|
||||
):
|
||||
name = mod_name
|
||||
|
||||
# XXX, lowlevel debuggin..
|
||||
# if pkg_name != _proj_name:
|
||||
# from tractor.devx.debug import mk_pdb
|
||||
# mk_pdb().set_trace()
|
||||
|
||||
if (
|
||||
name
|
||||
and
|
||||
name != _proj_name
|
||||
and
|
||||
name
|
||||
):
|
||||
|
||||
# NOTE: for handling for modules that use `get_logger(__name__)`
|
||||
# we make the following stylistic choice:
|
||||
# - always avoid duplicate project-package token
|
||||
|
|
@ -318,24 +410,63 @@ def get_logger(
|
|||
# since in python the {filename} is always this same
|
||||
# module-file.
|
||||
|
||||
sub_name: None|str = None
|
||||
rname, _, sub_name = name.partition('.')
|
||||
pkgpath, _, modfilename = sub_name.rpartition('.')
|
||||
rname: str = pkg_name
|
||||
pkg_path: str = name
|
||||
|
||||
# NOTE: for tractor itself never include the last level
|
||||
# module key in the name such that something like: eg.
|
||||
# 'tractor.trionics._broadcast` only includes the first
|
||||
# 2 tokens in the (coloured) name part.
|
||||
if rname == 'tractor':
|
||||
sub_name = pkgpath
|
||||
# ex. modden.runtime.progman
|
||||
# -> rname='modden', _, pkg_path='runtime.progman'
|
||||
if pkg_name in name:
|
||||
rname, _, pkg_path = name.partition('.')
|
||||
|
||||
if _root_name in sub_name:
|
||||
duplicate, _, sub_name = sub_name.partition('.')
|
||||
# ex. modden.runtime.progman
|
||||
# -> pkgpath='runtime', _, leaf_mod='progman'
|
||||
subpkg_path, _, leaf_mod = pkg_path.rpartition('.')
|
||||
|
||||
if not sub_name:
|
||||
# NOTE: special usage for passing `name=__name__`,
|
||||
#
|
||||
# - remove duplication of any root-pkg-name in the
|
||||
# (sub/child-)logger name; i.e. never include the
|
||||
# `pkg_name` *twice* in the top-most-pkg-name/level
|
||||
#
|
||||
# -> this happens normally since it is added to `.getChild()`
|
||||
# and as the name of its root-logger.
|
||||
#
|
||||
# => So for ex. (module key in the name) something like
|
||||
# `name='tractor.trionics._broadcast` is passed,
|
||||
# only includes the first 2 sub-pkg name-tokens in the
|
||||
# child-logger's name; the colored "pkg-namespace" header
|
||||
# will then correctly show the same value as `name`.
|
||||
if rname == pkg_name:
|
||||
pkg_path = subpkg_path
|
||||
|
||||
# XXX, do some double-checks for duplication of,
|
||||
# - root-pkg-name, already in root logger
|
||||
# - leaf-module-name already in `{filename}` header-field
|
||||
if pkg_name in pkg_path:
|
||||
_duplicate, _, pkg_path = pkg_path.partition('.')
|
||||
if _duplicate:
|
||||
# assert _duplicate == rname
|
||||
_root_log.warning(
|
||||
f'Duplicate pkg-name in sub-logger key?\n'
|
||||
f'pkg_name = {pkg_name!r}\n'
|
||||
f'pkg_path = {pkg_path!r}\n'
|
||||
)
|
||||
|
||||
if (
|
||||
leaf_mod
|
||||
and
|
||||
leaf_mod in pkg_path
|
||||
):
|
||||
_root_log.warning(
|
||||
f'Duplicate leaf-module-name in sub-logger key?\n'
|
||||
f'leaf_mod = {leaf_mod!r}\n'
|
||||
f'pkg_path = {pkg_path!r}\n'
|
||||
)
|
||||
|
||||
if not pkg_path:
|
||||
log = rlog
|
||||
else:
|
||||
log = rlog.getChild(sub_name)
|
||||
elif mk_sublog:
|
||||
log = rlog.getChild(pkg_path)
|
||||
|
||||
log.level = rlog.level
|
||||
|
||||
|
|
@ -350,8 +481,13 @@ def get_logger(
|
|||
for name, val in CUSTOM_LEVELS.items():
|
||||
logging.addLevelName(val, name)
|
||||
|
||||
# ensure customs levels exist as methods
|
||||
assert getattr(logger, name.lower()), f'Logger does not define {name}'
|
||||
# ensure our custom adapter levels exist as methods
|
||||
assert getattr(
|
||||
logger,
|
||||
name.lower()
|
||||
), (
|
||||
f'Logger does not define {name}'
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
|
@ -425,4 +561,4 @@ def get_loglevel() -> str:
|
|||
|
||||
|
||||
# global module logger for tractor itself
|
||||
log: StackLevelAdapter = get_logger('tractor')
|
||||
_root_log: StackLevelAdapter = get_logger('tractor')
|
||||
|
|
|
|||
62
uv.lock
62
uv.lock
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
|
|
@ -329,6 +329,32 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
|
|
@ -395,6 +421,24 @@ dev = [
|
|||
{ name = "typing-extensions" },
|
||||
{ name = "xonsh" },
|
||||
]
|
||||
devx = [
|
||||
{ name = "greenback" },
|
||||
{ name = "stackscope" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
lint = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
repl = [
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pyperclip" },
|
||||
{ name = "xonsh" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "pexpect" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
|
|
@ -420,6 +464,22 @@ dev = [
|
|||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||
]
|
||||
devx = [
|
||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||
]
|
||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
||||
repl = [
|
||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
||||
{ name = "psutil", specifier = ">=7.0.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||
]
|
||||
testing = [
|
||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
||||
{ name = "pytest", specifier = ">=8.3.5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tricycle"
|
||||
|
|
|
|||
Loading…
Reference in New Issue