strictcli 0.37.0__tar.gz → 0.38.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {strictcli-0.37.0 → strictcli-0.38.0}/PKG-INFO +1 -1
- {strictcli-0.37.0 → strictcli-0.38.0}/pyproject.toml +1 -1
- {strictcli-0.37.0 → strictcli-0.38.0}/strictcli/__init__.py +124 -9
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_effects_bypass_check.py +147 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/uv.lock +1 -1
- strictcli-0.37.0/.rlsbl/bases/.github/workflows/ci.yml +0 -33
- strictcli-0.37.0/.rlsbl/bases/.github/workflows/publish.yml +0 -195
- strictcli-0.37.0/.rlsbl/bases/.gitignore +0 -18
- strictcli-0.37.0/.rlsbl/lint/python.toml +0 -25
- strictcli-0.37.0/.rlsbl/version +0 -1
- {strictcli-0.37.0 → strictcli-0.38.0}/.claude/settings.json +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.github/workflows/ci.yml +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.github/workflows/publish.yml +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.gitignore +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.rlsbl/config.json +0 -0
- {strictcli-0.37.0/.rlsbl/bases → strictcli-0.38.0}/.rlsbl/lint/python.toml +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.rlsbl/managed-files.json +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/.strictcli/schema.json +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/CLAUDE.md +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/LICENSE +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/README.md +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/scripts/add_effect_classification.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/scripts/add_forwarding_declaration.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/strictcli/py.typed +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/conftest.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/flagship_app.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_arg_default.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_arg_default_validation.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_at_prefix.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_auto_version.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_call.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_command.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_discovery.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_provider.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_public_api.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_runner.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_schema.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_check_types.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_choices.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_choices_none.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_classification.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_command_help_suggestion.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_command_tags.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_compound_types.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_config.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_config_fields.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_config_file_path.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_config_set_bugs.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_confirm.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_connection_env.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_context.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_coverage.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_deep_nesting.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_dependencies.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_deprecated.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_dry_run_unsupported.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_dump_schema.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_e2e.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_effects.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_env.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_exit_codes.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_flag_sets.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_flagship_preview.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_float_format.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_float_type.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_float_vectors.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_global_flag_conflict_position.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_global_flags.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_guard_v2.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_help.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_hermetic.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_infra_env.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_int_type.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_invoke.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_keyword_flags.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_mcp.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_mutex.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_nesting.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_parser.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_passthrough.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_provenance.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_provenance_phase2.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_registration.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_repeatable.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_reserved_global_flags.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_reserved_quartet.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_tagdsl.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_toml_loading.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_tool_export.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_typed_args.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_unique.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_utilities.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_validate.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_variadic.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/tests/test_visibility.py +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/todo/.defer/deferred.md +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/todo/.done/keyword-collision-in-flag-param-name.md +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/todo/.done/original-idea.md +0 -0
- {strictcli-0.37.0 → strictcli-0.38.0}/todo/.done/public-check-runner-api.md +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "0.
|
|
5
|
+
__version__ = "0.38.0"
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"App", "Flag", "Arg", "FlagSet", "MutexGroup", "CoRequired", "Requires",
|
|
@@ -44,7 +44,7 @@ from collections import deque
|
|
|
44
44
|
from collections.abc import Sequence
|
|
45
45
|
from dataclasses import dataclass, field
|
|
46
46
|
from pathlib import Path
|
|
47
|
-
from typing import Any, Callable, Protocol, TypeVar, get_args, get_origin, runtime_checkable
|
|
47
|
+
from typing import Any, Callable, NamedTuple, Protocol, TypeVar, get_args, get_origin, runtime_checkable
|
|
48
48
|
|
|
49
49
|
# TypeVar for decorator return types — preserves the decorated function's type
|
|
50
50
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
@@ -1423,13 +1423,30 @@ def _validate_grants(cmd_name: str, grants) -> tuple:
|
|
|
1423
1423
|
# Closed lists, matched on the called attribute/function name: process starts,
|
|
1424
1424
|
# filesystem mutations, and network calls. The analyser is stdlib `ast` -- a
|
|
1425
1425
|
# regular dependency, no optional import and no soft degradation.
|
|
1426
|
+
#
|
|
1427
|
+
# Several leaves are RECEIVER-SCOPED: a name alone is not evidence of an
|
|
1428
|
+
# effect, and a finding a consumer cannot act on is worse than no finding.
|
|
1429
|
+
# `mapping.get(...)` is not a network call and `platform.system()` is not a
|
|
1430
|
+
# process start, so those leaves are banned only through a receiver the
|
|
1431
|
+
# module's own imports let the analyser resolve.
|
|
1426
1432
|
# ---------------------------------------------------------------------------
|
|
1427
1433
|
|
|
1428
1434
|
_BYPASS_PROCESS = frozenset({
|
|
1429
1435
|
"run", "Popen", "call", "check_call", "check_output", "getoutput",
|
|
1430
|
-
"getstatusoutput", "
|
|
1436
|
+
"getstatusoutput", "popen", "execv", "execvp", "execve",
|
|
1431
1437
|
"spawnv", "spawnl", "fork",
|
|
1432
1438
|
})
|
|
1439
|
+
# Process leaves that start a process only through `os`. `system` is the whole
|
|
1440
|
+
# set: `os.system` runs a shell, but `platform.system()` is a pure in-process
|
|
1441
|
+
# string read -- no process, no effect, and nothing the effects handle could
|
|
1442
|
+
# carry, since its closed method set has no in-process-observe method. Banning
|
|
1443
|
+
# the leaf on any receiver produced a finding whose own remediation ("route it
|
|
1444
|
+
# through ctx.effects") could not be followed, which is the one thing a lint
|
|
1445
|
+
# must never emit. An UNKNOWN receiver (`foo.system()`) is exempt for the same
|
|
1446
|
+
# reason the network leaves are receiver-scoped: without a resolvable binding
|
|
1447
|
+
# to `os` there is no evidence a process starts, and a name is not evidence.
|
|
1448
|
+
_BYPASS_PROCESS_OS_ONLY = frozenset({"system"})
|
|
1449
|
+
_BYPASS_OS_RECEIVERS = frozenset({"os"})
|
|
1433
1450
|
_BYPASS_FILESYSTEM = frozenset({
|
|
1434
1451
|
"remove", "unlink", "rmdir", "removedirs", "mkdir", "makedirs",
|
|
1435
1452
|
"rename", "renames", "replace", "chmod", "chown", "symlink", "link",
|
|
@@ -1453,6 +1470,63 @@ _BYPASS_SKIP_DIRS = frozenset({
|
|
|
1453
1470
|
"site-packages", "build", "dist", ".tox", ".mypy_cache", ".ruff_cache",
|
|
1454
1471
|
".pytest_cache", ".eggs",
|
|
1455
1472
|
})
|
|
1473
|
+
# The modules whose imports bind a receiver the analyser trusts. Closed, like
|
|
1474
|
+
# every other list here: `import requests as rq` must resolve to `requests` and
|
|
1475
|
+
# `from os import system` to `os`, while `from mylib import get` must bind
|
|
1476
|
+
# NOTHING -- widening this to every module would re-create exactly the ordinary
|
|
1477
|
+
# `mapping.get(...)` noise the network receiver list exists to remove.
|
|
1478
|
+
_BYPASS_EFFECT_MODULES = frozenset({
|
|
1479
|
+
"os", "os.path", "subprocess", "shutil", "pathlib", "socket", "tempfile",
|
|
1480
|
+
"requests", "httpx", "urllib", "urllib.request", "http", "http.client",
|
|
1481
|
+
"aiohttp", "urllib3",
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
class _BypassImports(NamedTuple):
|
|
1486
|
+
"""What a module's imports say about the names it calls.
|
|
1487
|
+
|
|
1488
|
+
``receivers`` maps a bound module name to the effect module it denotes
|
|
1489
|
+
(``import os as o`` -> ``{"o": "os"}``); ``calls`` maps a bound member name
|
|
1490
|
+
to the ``(module, member)`` pair it came from (``from os import system as
|
|
1491
|
+
sh`` -> ``{"sh": ("os", "system")}``). Both are used to normalize a call
|
|
1492
|
+
before the ban lists see it, so the lists stay written in terms of real
|
|
1493
|
+
module and member names rather than whatever the consumer spelled.
|
|
1494
|
+
"""
|
|
1495
|
+
|
|
1496
|
+
receivers: dict
|
|
1497
|
+
calls: dict
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
_BYPASS_NO_IMPORTS = _BypassImports({}, {})
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
def _bypass_import_bindings(tree) -> _BypassImports:
|
|
1504
|
+
"""Names bound to effect modules and to their members, in one module.
|
|
1505
|
+
|
|
1506
|
+
Relative imports are skipped: ``from .os import system`` is the consumer's
|
|
1507
|
+
own module, not the stdlib one, and the analyser cannot resolve it.
|
|
1508
|
+
"""
|
|
1509
|
+
receivers: dict = {}
|
|
1510
|
+
calls: dict = {}
|
|
1511
|
+
for node in ast.walk(tree):
|
|
1512
|
+
if isinstance(node, ast.Import):
|
|
1513
|
+
for alias in node.names:
|
|
1514
|
+
if alias.name not in _BYPASS_EFFECT_MODULES:
|
|
1515
|
+
continue
|
|
1516
|
+
if alias.asname is None:
|
|
1517
|
+
# `import os.path` binds `os`, and `os.system` still works.
|
|
1518
|
+
bound = module = alias.name.split(".")[0]
|
|
1519
|
+
else:
|
|
1520
|
+
bound = alias.asname
|
|
1521
|
+
module = alias.name.rsplit(".", 1)[-1]
|
|
1522
|
+
receivers[bound] = module
|
|
1523
|
+
elif isinstance(node, ast.ImportFrom):
|
|
1524
|
+
if node.level or node.module not in _BYPASS_EFFECT_MODULES:
|
|
1525
|
+
continue
|
|
1526
|
+
module = node.module.rsplit(".", 1)[-1]
|
|
1527
|
+
for alias in node.names:
|
|
1528
|
+
calls[alias.asname or alias.name] = (module, alias.name)
|
|
1529
|
+
return _BypassImports(receivers, calls)
|
|
1456
1530
|
|
|
1457
1531
|
|
|
1458
1532
|
def _call_target_name(node) -> tuple:
|
|
@@ -1613,10 +1687,45 @@ def _bypass_reachable_functions(tree) -> set:
|
|
|
1613
1687
|
return reachable
|
|
1614
1688
|
|
|
1615
1689
|
|
|
1616
|
-
def
|
|
1690
|
+
def _bypass_resolve_call(leaf: str, receiver,
|
|
1691
|
+
imports: _BypassImports) -> tuple:
|
|
1692
|
+
"""The ``(leaf, receiver)`` a call really goes through, per its imports.
|
|
1693
|
+
|
|
1694
|
+
A bare name imported from an effect module answers with that module and the
|
|
1695
|
+
member's REAL name (``from os import system as sh`` -> ``("system",
|
|
1696
|
+
"os")``), and an aliased module receiver answers with the module it denotes
|
|
1697
|
+
(``import os as o`` -> ``os``). Anything else is returned untouched, so an
|
|
1698
|
+
unresolvable receiver stays unresolvable rather than being guessed at.
|
|
1699
|
+
"""
|
|
1700
|
+
if receiver is None:
|
|
1701
|
+
bound = imports.calls.get(leaf)
|
|
1702
|
+
if bound is None:
|
|
1703
|
+
return leaf, None
|
|
1704
|
+
module, member = bound
|
|
1705
|
+
return member, module
|
|
1706
|
+
return leaf, imports.receivers.get(receiver, receiver)
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
def _bypass_call_is_banned(node, target: str, receiver,
|
|
1710
|
+
imports: _BypassImports = _BYPASS_NO_IMPORTS) -> bool:
|
|
1711
|
+
"""True when one call is a direct effect the handle should have carried.
|
|
1712
|
+
|
|
1713
|
+
Two leaves are deliberately narrower than the rest: builtin ``open`` is a
|
|
1714
|
+
finding only in a writing mode, and ``system`` only through ``os`` --
|
|
1715
|
+
``platform.system()`` observes this process and starts nothing, and the
|
|
1716
|
+
effects handle has no method that could carry it.
|
|
1717
|
+
|
|
1718
|
+
Builtin ``open`` is answered BEFORE import resolution, because it is the
|
|
1719
|
+
one leaf whose meaning comes from being unqualified. Everything after it is
|
|
1720
|
+
resolved through the module's imports (see :func:`_bypass_resolve_call`),
|
|
1721
|
+
so the lists below are written in terms of real module and member names.
|
|
1722
|
+
"""
|
|
1617
1723
|
leaf = target.rsplit(".", 1)[-1]
|
|
1618
1724
|
if leaf == "open" and receiver is None:
|
|
1619
1725
|
return _open_is_write_mode(node)
|
|
1726
|
+
leaf, receiver = _bypass_resolve_call(leaf, receiver, imports)
|
|
1727
|
+
if leaf in _BYPASS_PROCESS_OS_ONLY:
|
|
1728
|
+
return receiver in _BYPASS_OS_RECEIVERS
|
|
1620
1729
|
return (
|
|
1621
1730
|
(leaf in _BYPASS_PROCESS and receiver is not None)
|
|
1622
1731
|
or leaf in _BYPASS_FILESYSTEM
|
|
@@ -1625,7 +1734,7 @@ def _bypass_call_is_banned(node, target: str, receiver) -> bool:
|
|
|
1625
1734
|
|
|
1626
1735
|
|
|
1627
1736
|
def _bypass_walk(node, stack: list, reachable: set, findings: list, rel: str,
|
|
1628
|
-
aliases: frozenset) -> None:
|
|
1737
|
+
aliases: frozenset, imports: _BypassImports) -> None:
|
|
1629
1738
|
"""Walk one subtree, carrying the enclosing-function stack.
|
|
1630
1739
|
|
|
1631
1740
|
A banned call is reported once, at the INNERMOST enclosing function, when
|
|
@@ -1638,10 +1747,11 @@ def _bypass_walk(node, stack: list, reachable: set, findings: list, rel: str,
|
|
|
1638
1747
|
if stack and any(id(fn) in reachable for fn in stack):
|
|
1639
1748
|
if not _reaches_effects_handle(node.func, aliases):
|
|
1640
1749
|
target, receiver = _call_target_name(node.func)
|
|
1641
|
-
if target is not None and _bypass_call_is_banned(
|
|
1750
|
+
if target is not None and _bypass_call_is_banned(
|
|
1751
|
+
node, target, receiver, imports):
|
|
1642
1752
|
findings.append((rel, node.lineno, stack[-1].name, target))
|
|
1643
1753
|
for child in ast.iter_child_nodes(node):
|
|
1644
|
-
_bypass_walk(child, stack, reachable, findings, rel, aliases)
|
|
1754
|
+
_bypass_walk(child, stack, reachable, findings, rel, aliases, imports)
|
|
1645
1755
|
|
|
1646
1756
|
|
|
1647
1757
|
def _open_is_write_mode(node) -> bool:
|
|
@@ -1685,7 +1795,8 @@ def _scan_effects_bypasses(root: Path) -> list[tuple]:
|
|
|
1685
1795
|
if not reachable:
|
|
1686
1796
|
continue
|
|
1687
1797
|
_bypass_walk(tree, [], reachable, findings, rel,
|
|
1688
|
-
_bypass_effects_aliases(tree)
|
|
1798
|
+
_bypass_effects_aliases(tree),
|
|
1799
|
+
_bypass_import_bindings(tree))
|
|
1689
1800
|
return findings
|
|
1690
1801
|
|
|
1691
1802
|
|
|
@@ -4846,7 +4957,11 @@ class App:
|
|
|
4846
4957
|
|
|
4847
4958
|
- ``effects-bypass`` (error) fails on any direct process,
|
|
4848
4959
|
filesystem-mutation or network call REACHABLE FROM A REGISTERED
|
|
4849
|
-
COMMAND HANDLER
|
|
4960
|
+
COMMAND HANDLER. Its remediation is always "route it through
|
|
4961
|
+
ctx.effects", so a leaf the handle could not carry must never be a
|
|
4962
|
+
finding: the handle's closed method set has no in-process-observe
|
|
4963
|
+
method, which is why ``platform.system()`` is exempt while
|
|
4964
|
+
``os.system(...)`` is not (see :data:`_BYPASS_PROCESS_OS_ONLY`);
|
|
4850
4965
|
- ``observe-allowlist-breadth`` (warn) surfaces short
|
|
4851
4966
|
``proc_observe_allowlist`` prefixes, which authorize real execution
|
|
4852
4967
|
under ``--dry-run``;
|
|
@@ -326,6 +326,153 @@ async def deploy(ctx):
|
|
|
326
326
|
assert r.exit_code == 1
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
class TestSystemIsBannedOnlyThroughOs:
|
|
330
|
+
"""`system` starts a shell only as `os.system`.
|
|
331
|
+
|
|
332
|
+
`platform.system()` is a pure in-process string read: no process, no
|
|
333
|
+
effect, and NOTHING the effects handle could carry it -- the handle's
|
|
334
|
+
closed method set has no in-process-observe method. A finding on it is
|
|
335
|
+
unfixable by its own remediation ("route it through ctx.effects"), which
|
|
336
|
+
is the one thing a lint must never emit. So the leaf is receiver-scoped,
|
|
337
|
+
exactly as the network leaves already are.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def _run(self, tmp_path, source):
|
|
341
|
+
app, root = _app(tmp_path)
|
|
342
|
+
(root / "handlers.py").write_text(source)
|
|
343
|
+
return app.test(["check", "--name", "effects-bypass"])
|
|
344
|
+
|
|
345
|
+
def test_platform_system_is_not_a_finding(self, tmp_path):
|
|
346
|
+
r = self._run(tmp_path, '''
|
|
347
|
+
import platform
|
|
348
|
+
|
|
349
|
+
def deploy(ctx):
|
|
350
|
+
ctx.effects.run(["true"])
|
|
351
|
+
return platform.system()
|
|
352
|
+
''')
|
|
353
|
+
assert r.exit_code == 0, r.stdout
|
|
354
|
+
assert "no direct effect calls bypass ctx.effects" in r.stdout
|
|
355
|
+
|
|
356
|
+
def test_system_imported_from_platform_is_not_a_finding(self, tmp_path):
|
|
357
|
+
r = self._run(tmp_path, '''
|
|
358
|
+
from platform import system
|
|
359
|
+
|
|
360
|
+
def deploy(ctx):
|
|
361
|
+
ctx.effects.run(["true"])
|
|
362
|
+
return system()
|
|
363
|
+
''')
|
|
364
|
+
assert r.exit_code == 0, r.stdout
|
|
365
|
+
|
|
366
|
+
def test_an_unknown_receiver_named_system_is_not_a_finding(self, tmp_path):
|
|
367
|
+
"""No resolvable binding to `os` is no evidence a process starts.
|
|
368
|
+
|
|
369
|
+
`foo.system()` is some object's method. Receiver-awareness means the
|
|
370
|
+
lint reports what it can show, and an unknown receiver shows nothing.
|
|
371
|
+
"""
|
|
372
|
+
r = self._run(tmp_path, '''
|
|
373
|
+
def deploy(ctx, foo):
|
|
374
|
+
ctx.effects.run(["true"])
|
|
375
|
+
return foo.system()
|
|
376
|
+
''')
|
|
377
|
+
assert r.exit_code == 0, r.stdout
|
|
378
|
+
|
|
379
|
+
def test_os_system_is_still_a_finding(self, tmp_path):
|
|
380
|
+
r = self._run(tmp_path, '''
|
|
381
|
+
import os
|
|
382
|
+
|
|
383
|
+
def deploy(ctx):
|
|
384
|
+
ctx.effects.run(["true"])
|
|
385
|
+
os.system("rm -rf /")
|
|
386
|
+
''')
|
|
387
|
+
assert r.exit_code == 1
|
|
388
|
+
assert "deploy calls os.system directly" in r.stdout
|
|
389
|
+
|
|
390
|
+
def test_system_imported_from_os_is_a_finding(self, tmp_path):
|
|
391
|
+
r = self._run(tmp_path, '''
|
|
392
|
+
from os import system
|
|
393
|
+
|
|
394
|
+
def deploy(ctx):
|
|
395
|
+
ctx.effects.run(["true"])
|
|
396
|
+
system("rm -rf /")
|
|
397
|
+
''')
|
|
398
|
+
assert r.exit_code == 1, r.stdout
|
|
399
|
+
assert "deploy calls system directly" in r.stdout
|
|
400
|
+
|
|
401
|
+
def test_system_imported_from_os_under_an_alias_is_a_finding(self, tmp_path):
|
|
402
|
+
r = self._run(tmp_path, '''
|
|
403
|
+
from os import system as shell_out
|
|
404
|
+
|
|
405
|
+
def deploy(ctx):
|
|
406
|
+
ctx.effects.run(["true"])
|
|
407
|
+
shell_out("rm -rf /")
|
|
408
|
+
''')
|
|
409
|
+
assert r.exit_code == 1, r.stdout
|
|
410
|
+
assert "deploy calls shell_out directly" in r.stdout
|
|
411
|
+
|
|
412
|
+
def test_an_aliased_os_module_receiver_is_a_finding(self, tmp_path):
|
|
413
|
+
r = self._run(tmp_path, '''
|
|
414
|
+
import os as o
|
|
415
|
+
|
|
416
|
+
def deploy(ctx):
|
|
417
|
+
ctx.effects.run(["true"])
|
|
418
|
+
o.system("rm -rf /")
|
|
419
|
+
''')
|
|
420
|
+
assert r.exit_code == 1, r.stdout
|
|
421
|
+
assert "deploy calls o.system directly" in r.stdout
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TestImportBindingsResolveReceivers:
|
|
425
|
+
"""An effect-module import is a receiver the analyser can resolve.
|
|
426
|
+
|
|
427
|
+
The same binding table that keeps `platform.system` clean also closes two
|
|
428
|
+
escapes it would otherwise open: an aliased effect module and a bare name
|
|
429
|
+
imported from one.
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
def _run(self, tmp_path, source):
|
|
433
|
+
app, root = _app(tmp_path)
|
|
434
|
+
(root / "handlers.py").write_text(source)
|
|
435
|
+
return app.test(["check", "--name", "effects-bypass"])
|
|
436
|
+
|
|
437
|
+
def test_an_aliased_requests_module_is_a_network_finding(self, tmp_path):
|
|
438
|
+
r = self._run(tmp_path, '''
|
|
439
|
+
import requests as rq
|
|
440
|
+
|
|
441
|
+
def deploy(ctx):
|
|
442
|
+
ctx.effects.run(["true"])
|
|
443
|
+
rq.post("https://x.test")
|
|
444
|
+
''')
|
|
445
|
+
assert r.exit_code == 1, r.stdout
|
|
446
|
+
assert "deploy calls rq.post directly" in r.stdout
|
|
447
|
+
|
|
448
|
+
def test_a_bare_name_imported_from_subprocess_is_a_finding(self, tmp_path):
|
|
449
|
+
r = self._run(tmp_path, '''
|
|
450
|
+
from subprocess import run
|
|
451
|
+
|
|
452
|
+
def deploy(ctx):
|
|
453
|
+
ctx.effects.run(["true"])
|
|
454
|
+
run(["git", "push"])
|
|
455
|
+
''')
|
|
456
|
+
assert r.exit_code == 1, r.stdout
|
|
457
|
+
assert "deploy calls run directly" in r.stdout
|
|
458
|
+
|
|
459
|
+
def test_a_bare_name_from_an_unrelated_module_is_not_a_finding(self, tmp_path):
|
|
460
|
+
"""Only the closed list of effect modules binds a receiver.
|
|
461
|
+
|
|
462
|
+
`from mylib import get` must stay silent -- widening the binding table
|
|
463
|
+
to every module would re-create the `mapping.get(...)` noise the
|
|
464
|
+
network receiver list exists to remove.
|
|
465
|
+
"""
|
|
466
|
+
r = self._run(tmp_path, '''
|
|
467
|
+
from mylib import get
|
|
468
|
+
|
|
469
|
+
def deploy(ctx):
|
|
470
|
+
ctx.effects.run(["true"])
|
|
471
|
+
return get("k")
|
|
472
|
+
''')
|
|
473
|
+
assert r.exit_code == 0, r.stdout
|
|
474
|
+
|
|
475
|
+
|
|
329
476
|
class TestObserveAllowlistBreadth:
|
|
330
477
|
"""§6.2's hazard, surfaced as a WARNING and never as an error.
|
|
331
478
|
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
workflow_dispatch:
|
|
9
|
-
|
|
10
|
-
# Per-SHA group: re-runs of the same commit dedupe, but a new commit never
|
|
11
|
-
# cancels an earlier commit's in-flight run (release CI conclusions stay intact).
|
|
12
|
-
concurrency:
|
|
13
|
-
group: ${{ github.workflow_ref }}-${{ github.sha }}
|
|
14
|
-
cancel-in-progress: true
|
|
15
|
-
|
|
16
|
-
jobs:
|
|
17
|
-
test:
|
|
18
|
-
runs-on: ubuntu-latest
|
|
19
|
-
strategy:
|
|
20
|
-
matrix:
|
|
21
|
-
# requires-python: >= 3.11
|
|
22
|
-
python-version: ["3.12", "3.13", "3.14"]
|
|
23
|
-
steps:
|
|
24
|
-
- uses: actions/checkout@v6
|
|
25
|
-
- uses: astral-sh/setup-uv@v7
|
|
26
|
-
- run: uv python install ${{ matrix.python-version }}
|
|
27
|
-
- run: uv sync --locked
|
|
28
|
-
- run: uv run python -c "import strictcli"
|
|
29
|
-
- name: Install gitleaks
|
|
30
|
-
run: |
|
|
31
|
-
GITLEAKS_VERSION=8.24.3
|
|
32
|
-
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
33
|
-
- run: uv run pytest --rootdir .
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
on:
|
|
3
|
-
release:
|
|
4
|
-
types: [published]
|
|
5
|
-
workflow_dispatch:
|
|
6
|
-
inputs:
|
|
7
|
-
tag:
|
|
8
|
-
description: Release tag to publish (e.g. v1.2.3). Overrides the ref for
|
|
9
|
-
retry dispatch.
|
|
10
|
-
required: false
|
|
11
|
-
type: string
|
|
12
|
-
|
|
13
|
-
# One publish run per tag: a workflow_dispatch retry at the same tag
|
|
14
|
-
# queues behind the in-flight run instead of racing it. A publish is never
|
|
15
|
-
# cancelled mid-flight.
|
|
16
|
-
concurrency:
|
|
17
|
-
group: publish-${{ inputs.tag || github.ref_name }}
|
|
18
|
-
cancel-in-progress: false
|
|
19
|
-
permissions:
|
|
20
|
-
contents: read
|
|
21
|
-
id-token: write
|
|
22
|
-
jobs:
|
|
23
|
-
gate:
|
|
24
|
-
name: Gate on CI
|
|
25
|
-
runs-on: ubuntu-latest
|
|
26
|
-
permissions:
|
|
27
|
-
checks: read
|
|
28
|
-
contents: read
|
|
29
|
-
env:
|
|
30
|
-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
31
|
-
GH_REPO: ${{ github.repository }}
|
|
32
|
-
GATE_TIMEOUT_MINUTES: '20'
|
|
33
|
-
GATE_GRACE_MINUTES: '5'
|
|
34
|
-
GATE_POLL_SECONDS: '15'
|
|
35
|
-
GATE_MARKER_ATTEMPTS: '5'
|
|
36
|
-
GATE_MARKER_RETRY_SECONDS: '5'
|
|
37
|
-
CI_CHECK_REGEX: ^(test)( \(.*\))?$
|
|
38
|
-
steps:
|
|
39
|
-
- name: Wait for CI to succeed on the release commit
|
|
40
|
-
run: |
|
|
41
|
-
set -euo pipefail
|
|
42
|
-
|
|
43
|
-
# Commit resolution -- explicit marker-first order (never reads the release
|
|
44
|
-
# event payload; dispatch retries have none, so this path is uniform for
|
|
45
|
-
# release events and dispatch retries alike):
|
|
46
|
-
# (1) Marker: rlsbl writes the exact commit CI ran on into the GitHub
|
|
47
|
-
# Release body as a machine-parseable line of the form
|
|
48
|
-
# <!-- rlsbl-ci-sha: <40-hex> -->
|
|
49
|
-
# Prefer it when present -- it pins the precise commit and is immune to
|
|
50
|
-
# ref races. A just-created release can lag on a GitHub API read replica
|
|
51
|
-
# (the marker is written at release creation but may not be visible on
|
|
52
|
-
# the first read), so RETRY the read GATE_MARKER_ATTEMPTS times,
|
|
53
|
-
# GATE_MARKER_RETRY_SECONDS apart, before concluding it is absent. The
|
|
54
|
-
# tag is inputs.tag (TAG_INPUT) when dispatched with an explicit
|
|
55
|
-
# override, else GITHUB_REF_NAME (matches the router resolver).
|
|
56
|
-
# (2) Fallback: older releases predate the marker, so fall back to
|
|
57
|
-
# $GITHUB_SHA, the tag's commit for both release and dispatch-at-tag.
|
|
58
|
-
extract_ci_sha() {
|
|
59
|
-
# Emit the first rlsbl-ci-sha marker SHA found on stdin, if any.
|
|
60
|
-
sed -n 's/.*<!-- rlsbl-ci-sha: \([0-9a-f]\{40\}\) -->.*/\1/p' | head -n1
|
|
61
|
-
}
|
|
62
|
-
tag="${TAG_INPUT:-$GITHUB_REF_NAME}"
|
|
63
|
-
marker_attempts="${GATE_MARKER_ATTEMPTS:-5}"
|
|
64
|
-
marker_retry_seconds="${GATE_MARKER_RETRY_SECONDS:-5}"
|
|
65
|
-
sha=""
|
|
66
|
-
attempt=1
|
|
67
|
-
while [ "$attempt" -le "$marker_attempts" ]; do
|
|
68
|
-
if body="$(gh release view "$tag" --json body --jq .body 2>/dev/null)"; then
|
|
69
|
-
sha="$(printf '%s\n' "$body" | extract_ci_sha)"
|
|
70
|
-
fi
|
|
71
|
-
if [ -n "$sha" ]; then
|
|
72
|
-
break
|
|
73
|
-
fi
|
|
74
|
-
if [ "$attempt" -lt "$marker_attempts" ]; then
|
|
75
|
-
echo "Publish gate: rlsbl-ci-sha marker not yet visible in the '$tag' release body (attempt $attempt/$marker_attempts); retrying in ${marker_retry_seconds}s..."
|
|
76
|
-
sleep "$marker_retry_seconds"
|
|
77
|
-
fi
|
|
78
|
-
attempt=$(( attempt + 1 ))
|
|
79
|
-
done
|
|
80
|
-
if [ -n "$sha" ]; then
|
|
81
|
-
echo "Publish gate: resolved release commit from rlsbl-ci-sha marker in the '$tag' release body."
|
|
82
|
-
else
|
|
83
|
-
sha="$GITHUB_SHA"
|
|
84
|
-
echo "Publish gate: no rlsbl-ci-sha marker after $marker_attempts attempt(s) on the '$tag' release body; falling back to \$GITHUB_SHA."
|
|
85
|
-
fi
|
|
86
|
-
echo "Publish gate: waiting for CI on $GITHUB_REF_NAME (commit $sha)"
|
|
87
|
-
echo "Check-run name filter: $CI_CHECK_REGEX"
|
|
88
|
-
# Timeout, grace window, and poll interval come from the job env above;
|
|
89
|
-
# edit them there if this repository's CI needs different limits.
|
|
90
|
-
|
|
91
|
-
now() { date +%s; }
|
|
92
|
-
start="$(now)"
|
|
93
|
-
deadline=$(( start + GATE_TIMEOUT_MINUTES * 60 ))
|
|
94
|
-
grace_deadline=$(( start + GATE_GRACE_MINUTES * 60 ))
|
|
95
|
-
|
|
96
|
-
while :; do
|
|
97
|
-
if ! resp="$(gh api --paginate "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?per_page=100")"; then
|
|
98
|
-
echo "Checks API request failed; retrying in ${GATE_POLL_SECONDS}s..."
|
|
99
|
-
sleep "$GATE_POLL_SECONDS"
|
|
100
|
-
continue
|
|
101
|
-
fi
|
|
102
|
-
# Match this project's CI check runs by name; exclude check runs that
|
|
103
|
-
# belong to THIS workflow run (the gate itself and the queued publish
|
|
104
|
-
# jobs would otherwise deadlock the poll loop).
|
|
105
|
-
# A retried CI run creates a BRAND-NEW check-run with the same name as the
|
|
106
|
-
# old one, so a stale failure would block the gate forever. Project id and
|
|
107
|
-
# started_at, then reduce to the latest check-run per name (max started_at,
|
|
108
|
-
# numeric id as tiebreak for retries within the same second) BEFORE the
|
|
109
|
-
# pending / not-success logic runs. This way the newest run decides: still
|
|
110
|
-
# running -> wait; genuinely red -> hard fail.
|
|
111
|
-
runs="$(jq -s --arg re "$CI_CHECK_REGEX" --arg run_id "$GITHUB_RUN_ID" '
|
|
112
|
-
[ .[].check_runs[]
|
|
113
|
-
| select(.name | test($re))
|
|
114
|
-
| select((.details_url // "") | contains("/actions/runs/" + $run_id + "/") | not)
|
|
115
|
-
| {name, status, conclusion, id, started_at} ]
|
|
116
|
-
| group_by(.name)
|
|
117
|
-
| map(sort_by(.started_at, .id) | last)' <<< "$resp")"
|
|
118
|
-
total="$(jq 'length' <<< "$runs")"
|
|
119
|
-
|
|
120
|
-
if [ "$total" -eq 0 ]; then
|
|
121
|
-
if [ "$(now)" -ge "$grace_deadline" ]; then
|
|
122
|
-
echo "::error::Publish gate: no CI check runs matching $CI_CHECK_REGEX appeared on $sha within $GATE_GRACE_MINUTES minutes."
|
|
123
|
-
echo "A scaffolded repository always has a CI workflow, and rlsbl verifies CI on this exact commit BEFORE tagging it, so check runs must exist here."
|
|
124
|
-
echo "Their absence means the check runs were deleted, the commit resolution is wrong, or this tag was created outside rlsbl."
|
|
125
|
-
echo "If CI jobs were renamed, update CI_CHECK_REGEX in this workflow's gate job to match the new names."
|
|
126
|
-
exit 1
|
|
127
|
-
fi
|
|
128
|
-
echo "No matching CI check runs yet; retrying in ${GATE_POLL_SECONDS}s..."
|
|
129
|
-
sleep "$GATE_POLL_SECONDS"
|
|
130
|
-
continue
|
|
131
|
-
fi
|
|
132
|
-
|
|
133
|
-
pending="$(jq '[ .[] | select(.status != "completed") ] | length' <<< "$runs")"
|
|
134
|
-
if [ "$pending" -gt 0 ]; then
|
|
135
|
-
if [ "$(now)" -ge "$deadline" ]; then
|
|
136
|
-
echo "::error::Publish gate: timed out after $GATE_TIMEOUT_MINUTES minutes waiting for CI to complete on $sha."
|
|
137
|
-
jq -r '.[] | " \(.name): status=\(.status) conclusion=\(.conclusion // "none")"' <<< "$runs"
|
|
138
|
-
exit 1
|
|
139
|
-
fi
|
|
140
|
-
echo "$pending of $total matching CI check runs still running; retrying in ${GATE_POLL_SECONDS}s..."
|
|
141
|
-
sleep "$GATE_POLL_SECONDS"
|
|
142
|
-
continue
|
|
143
|
-
fi
|
|
144
|
-
|
|
145
|
-
not_success="$(jq '[ .[] | select(.conclusion != "success") ]' <<< "$runs")"
|
|
146
|
-
if [ "$(jq 'length' <<< "$not_success")" -gt 0 ]; then
|
|
147
|
-
echo "::error::Publish gate: CI did not pass on $sha -- refusing to publish."
|
|
148
|
-
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$not_success"
|
|
149
|
-
while IFS= read -r conclusion; do
|
|
150
|
-
case "$conclusion" in
|
|
151
|
-
failure|timed_out)
|
|
152
|
-
echo "CI concluded '$conclusion' on the release commit."
|
|
153
|
-
echo "rlsbl tags and releases a commit only AFTER its CI has gone green, so reaching this branch means one of: CI was re-run on an already-released commit and regressed, a required check was added after the release, or this tag/Release was created outside rlsbl."
|
|
154
|
-
echo "Do NOT re-dispatch this publish workflow expecting a different answer -- a failure baked into the code at this commit fails identically every time, and there is no way to make this tag green."
|
|
155
|
-
echo "Remedy: fix forward on the release branch and cut a NEW release with 'rlsbl release run' (its own CI gate must go green before it is tagged), then mark this one with 'rlsbl release deprecate <version>'."
|
|
156
|
-
;;
|
|
157
|
-
cancelled)
|
|
158
|
-
echo "A CI check run was CANCELLED. A cancelled run proves nothing about the commit, so the gate treats it as a hard failure instead of waiting for a conclusion that will never come."
|
|
159
|
-
echo "Remedy: re-run the cancelled CI workflow on this exact commit (gh run rerun <run-id>). If it concludes success, re-dispatch this publish workflow at the tag ref: gh workflow run <publish workflow> --ref $GITHUB_REF_NAME"
|
|
160
|
-
;;
|
|
161
|
-
skipped)
|
|
162
|
-
echo "A CI check run matching the filter was SKIPPED. The gate cannot treat a skipped check as passing: this project must actually run its own CI on the release commit."
|
|
163
|
-
echo "Remedy: check paths filters and job conditions so CI runs for this project, re-run CI on this commit, then re-dispatch this publish workflow at the tag ref."
|
|
164
|
-
;;
|
|
165
|
-
*)
|
|
166
|
-
echo "CI check concluded '$conclusion' (not success). The gate only proceeds when every matching check concluded success."
|
|
167
|
-
;;
|
|
168
|
-
esac
|
|
169
|
-
done <<< "$(jq -r '.[].conclusion' <<< "$not_success" | sort -u)"
|
|
170
|
-
exit 1
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
echo "Publish gate: all $total matching CI check runs succeeded."
|
|
174
|
-
jq -r '.[] | " \(.name): \(.conclusion)"' <<< "$runs"
|
|
175
|
-
exit 0
|
|
176
|
-
done
|
|
177
|
-
pypi:
|
|
178
|
-
needs: gate
|
|
179
|
-
runs-on: ubuntu-latest
|
|
180
|
-
steps:
|
|
181
|
-
- uses: actions/checkout@v6
|
|
182
|
-
with:
|
|
183
|
-
ref: ${{ inputs.tag || github.event.release.tag_name }}
|
|
184
|
-
- uses: astral-sh/setup-uv@v7
|
|
185
|
-
- name: Install gitleaks
|
|
186
|
-
run: |
|
|
187
|
-
GITLEAKS_VERSION=8.24.3
|
|
188
|
-
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar xz -C /usr/local/bin gitleaks
|
|
189
|
-
- run: uv build --out-dir dist
|
|
190
|
-
- name: Scan artifacts for secrets
|
|
191
|
-
run: |
|
|
192
|
-
gitleaks dir dist/
|
|
193
|
-
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
194
|
-
with:
|
|
195
|
-
skip-existing: true
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
node_modules/
|
|
2
|
-
__pycache__/
|
|
3
|
-
*.pyc
|
|
4
|
-
*.log
|
|
5
|
-
.DS_Store
|
|
6
|
-
coverage/
|
|
7
|
-
build/
|
|
8
|
-
dist/
|
|
9
|
-
target/
|
|
10
|
-
*.egg-info/
|
|
11
|
-
.rlsbl-notes-*.tmp
|
|
12
|
-
.rlsbl/lock
|
|
13
|
-
.rlsbl-monorepo/lock
|
|
14
|
-
.credentials.json
|
|
15
|
-
.*-cache.json
|
|
16
|
-
.env
|
|
17
|
-
.env.local
|
|
18
|
-
*.local-only
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[forbidden-imports]
|
|
2
|
-
modules = [
|
|
3
|
-
"argparse",
|
|
4
|
-
"click",
|
|
5
|
-
"typer",
|
|
6
|
-
"flask",
|
|
7
|
-
"fastapi",
|
|
8
|
-
"django",
|
|
9
|
-
"uvicorn",
|
|
10
|
-
"granian",
|
|
11
|
-
"starlette",
|
|
12
|
-
"tornado",
|
|
13
|
-
"bottle",
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
[stdout]
|
|
17
|
-
enabled = true
|
|
18
|
-
ignore = []
|
|
19
|
-
|
|
20
|
-
[entry-point]
|
|
21
|
-
enabled = true
|
|
22
|
-
ignore = []
|
|
23
|
-
|
|
24
|
-
[files]
|
|
25
|
-
exclude = []
|
strictcli-0.37.0/.rlsbl/version
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.110.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|