Analytics linter — describe your system in Python, validate consistency
Project description
analint
A Python DSL for declaring and verifying how a system behaves.
Requirements live in Word, Confluence, and Miro — readable by humans, but impossible to check for contradictions, diff, or hand to an AI agent. Code is the other extreme: too low-level. analint sits in the middle: Python that reads like a specification and checks like one.
checkout = Action(
pre=[
Wallet.balance >= Order.total,
Order.status == OrderStatus.PENDING,
],
effect=[
Set(Order.status, OrderStatus.PAID),
Subtract(Wallet.balance, Order.total),
],
)
sc_broke = Scenario(
name="Insufficient balance",
action=checkout,
given=[
Order(id="o1", total=50.0, customer_id="c1"),
Wallet(balance=10.0, customer_id="c1"), # 10 < 50 → blocked
],
expected=Expect.FAIL,
)
analint examples/ecommerce/
SCENARIOS
PASS checkout/happy (6 rules)
PASS checkout/no-funds (6 rules)
↳ PRE failed: Wallet.balance >= Order.total
↳ correctly blocked — rules rejected this data as expected
It is not limited to business analytics: the same primitives describe game rules and narrative consistency — see examples/cloak/, the classic Cloak of Darkness IF benchmark expressed as a verifiable spec.
Installation
pip install analint
# or with uv:
uv add analint
Quick start
Create spec.py and run analint . in the same directory.
from analint import Entity, Action, Scenario, Spec, Expect
class Item(Entity):
price: float
stock: int
class Budget(Entity):
amount: float
buy = Action(
pre=[
Item.price > 0,
Budget.amount >= Item.price,
],
)
sc_ok = Scenario(
name="Happy path",
action=buy,
given=[Item(price=10.0, stock=5), Budget(amount=20.0)],
)
spec = Spec(id="shop", name="Shop") # everything above is discovered automatically
ids are derived from variable names (buy, sc_ok) — set id= explicitly only when you want a different one.
DSL reference
The DSL has three layers: state (Entity, Event), constraints (Invariant, predicates), and transitions (Action, Lifecycle). Scenarios and Flows tie them together with concrete examples.
Entity
Typed system state. Annotate fields normally; class-level access returns a
FieldDescriptor usable in predicate expressions. Use Field(...) for
constraints on one value, and attach a Lifecycle(...) directly to a field
whose states have declared transitions.
from enum import StrEnum
from analint import Entity, Field, Lifecycle
class OrderStatus(StrEnum):
PENDING = "pending"
PAID = "paid"
CANCELLED = "cancelled"
class Order(Entity):
status: OrderStatus = Lifecycle(
initial=OrderStatus.PENDING,
transitions={
OrderStatus.PENDING: [OrderStatus.PAID, OrderStatus.CANCELLED],
OrderStatus.PAID: [OrderStatus.CANCELLED],
},
terminal=[OrderStatus.CANCELLED],
)
total: float = Field(gt=0) # required, must be positive
customer_id: str
Order.total # class level → FieldDescriptor (for predicates)
Order(total=50.0, customer_id="c1").total # instance level → 50.0
Field(default, ge=..., gt=..., le=..., lt=...) validates constructed
instances, checks post-action values, and supplies finite numeric bounds to the
reachability engine. Field(default, values=[...]) declares an explicit
finite scalar domain. With saturate=True, numeric values clamp at the
declared ge/le thresholds instead of making the transition invalid.
Predicates
Comparison operators on fields build predicate objects; combinators are plain functions:
Order.total > 0
Order.status == OrderStatus.PENDING
Wallet.balance >= Order.total # field-to-field comparison
from analint import And, Or, Not, Implies, In, IsNull, IsNotNull
And(Order.total > 0, Wallet.balance >= Order.total)
Implies(Hook.holds_cloak, Not(Player.has_cloak)) # if A then B
In(Order.status, [OrderStatus.PAID, OrderStatus.CANCELLED])
Arithmetic over fields builds expression nodes — including named, reusable ones (Quint-style derived values):
Wallet.balance - Order.total >= 0
total_supply = Alice.coins + Bob.coins + Eve.coins # named expression
supply_fits = AlwaysHolds(total_supply <= MAX_SUPPLY)
Predicates are values — name them and reuse them:
board_is_active = Board.status == BoardStatus.ACTIVE
create_card = Action(pre=[board_is_active, ...])
archive_card = Action(pre=[board_is_active, ...])
Invariant
A relation that must hold in every state — checked before an action and
re-checked after its effects. Put constraints involving one field on
Field(...); use Invariant for relationships between fields or entities.
from analint import Implies, Invariant
delivered_means_paid = Invariant(
Implies(
Order.status == OrderStatus.DELIVERED,
Payment.status == PaymentStatus.CAPTURED,
),
label="A delivered order must have a captured payment",
)
An invariant is skipped in scenarios whose given does not include the entities it references.
Action
A state transition: what must hold before (pre), what changes (effect), and
what must hold after (post).
from analint import Action, Set, Subtract
checkout = Action(
name="Customer Checkout",
pre=[
Wallet.balance >= Order.total,
Product.stock > 0,
Order.status == OrderStatus.PENDING,
],
effect=[ # facts about the next state
Set(Order.status, OrderStatus.PAID),
Subtract(Wallet.balance, Order.total),
Subtract(Product.stock, 1),
],
post=[Order.status == OrderStatus.PAID], # optional double-entry check
emits=[OrderPlaced(order_id=Order.id, total=Order.total, customer_id=Order.customer_id)],
)
Effects are simultaneous facts, not a program. Every right-hand side is evaluated against the pre-state; the order of the list carries no meaning; two effects on the same field are a structural error.
Current semantic boundary: event-driven causality is modelled through state, not a dispatch primitive.
| Effect | Next-state fact |
|---|---|
Set(field, value) |
field becomes the value — a literal, an enum, or an expression over the pre-state: Set(src.coins, src.coins - amount) is the canonical form |
Subtract(field, amount) / Add(field, amount) |
sugar for Set(field, field ∓ amount) |
Create(ref, **fields) / Delete(ref) |
a slot in a Scope becomes present / absent — presence as a next-state fact (see Bounded multiplicity) |
Parameterized actions
A family of similar transitions is one declaration over finite domains — never a host-language loop:
from analint import Param
src = Param("src", AliceCoins, BobCoins, EveCoins)
dst = Param("dst", AliceCoins, BobCoins, EveCoins)
amount = Param("amount", ge=1, le=3) # integer range — same vocabulary as Field
send = Action(
params=[src, dst, amount],
where=[src != dst],
pre=[src.coins >= amount, dst.coins <= MAX_BALANCE - amount],
effect=[Subtract(src.coins, amount), Add(dst.coins, amount)],
)
The engine expands every binding that satisfies where into a concrete
action (send(src=AliceCoins, dst=BobCoins, amount=2)); traces and reports
use the parametric names. A scenario picks one binding:
action=send.bind(src=BobCoins, dst=EveCoins, amount=2).
Bounded multiplicity
Scope declares a fixed finite set of identified instances of one entity
type. Instance refs work in predicates and effects, and can be a Param
domain:
from analint import Absent, Present, Scope
class Account(Entity):
balance: int = Field(0, ge=0, le=5)
accounts = Scope(Account, keys=["alice", "bob", "eve"])
alice = accounts["alice"]
src = Param("src", accounts)
dst = Param("dst", accounts)
transfer = Action(
params=[src, dst],
where=[src != dst],
pre=[src.balance >= 1],
effect=[Subtract(src.balance, 1), Add(dst.balance, 1)],
)
alice_pays_bob = Scenario(
action=transfer.bind(src=alice, dst=accounts["bob"]),
given=[
alice(balance=3),
accounts["bob"](balance=0),
Absent(accounts["eve"]),
],
)
alice_exists = Present(alice)
The universe is fixed, but membership is explicit. A normal scoped snapshot is
present; Absent(ref) marks an allocated slot as absent; Present(ref) is a
predicate usable in guards and queries. In scenarios, omitted scoped slots are
also absent. Reachability defaults every scoped slot to present unless
given=[Absent(ref)] says otherwise. Field reads and ordinary effects on an
absent slot are rejected.
Create(ref, **fields) and Delete(ref) change that membership as next-state
facts, symmetric to Set over a value:
from analint import Create, Delete
open_account = Action(effect=[Create(accounts["eve"], balance=0)]) # eve must be absent
close_account = Action(effect=[Delete(alice)]) # alice must be present
Create requires the slot absent and makes it present (unspecified fields take
their defaults); Delete requires it present and makes it absent. The
mismatched pre-state is rejected before any effect runs, so Expect.FAIL
covers it. A slot's presence may change at most once per action, and the same
slot may not be both (de)allocated and written by Set/Add/Subtract. The
key universe stays fixed, so reachability, quantifiers and aggregates simply
range over whichever slots are present in each state.
Finite quantifiers
Bound introduces a named variable over a Scope; ForAll and Exists
remain explicit predicate AST nodes and work in invariants, action guards,
scenario assertions, and reachability queries:
from analint import Bound, Count, Exists, ForAll, Max, Min, Sum
account = Bound("account", accounts)
all_balances_valid = AlwaysHolds(
ForAll(account, account.balance >= 0)
)
someone_is_full = Reachable(
Exists(account, account.balance == 5)
)
total_balance = Sum(account, account.balance)
nonempty_accounts = Count(account, account.balance > 0)
balance_range = Max(account, account.balance) - Min(account, account.balance)
Quantifiers and aggregates are finite and exhaustive over the present
instances in the registered scope. Aggregate nodes are ordinary arithmetic
expressions: they can be compared, composed with field math, used in effect
right-hand sides, and checked by the reachability engine. Over an empty
present set, ForAll is true, Exists is false, Count and Sum are zero,
while Min/Max report an evaluation error.
Event
An observable domain fact, with a typed payload. Emitting binds payload fields to expressions over the state (the kernel materialises them). An action's pre may constrain an event payload by referencing the event's fields (the event instance lives in the scenario's given):
from analint import Event
class OrderPlaced(Event):
order_id: str
total: float
checkout = Action(..., emits=[OrderPlaced(order_id=Order.id, total=Order.total)])
notify_vip = Action(
pre=[OrderPlaced.total > 100], # payload condition
effect=[Add(Manager.alerts, 1)],
)
The linter validates payload bindings (fields exist, types match). Emitting an
event does not trigger another action — event-driven causality is modelled
through state (see examples/fulfillment, a saga chained via status fields).
Lifecycle
Valid states of an entity field and the allowed transitions between them. The lifecycle is declared as the field's default, so the state definition, initial value, transitions, and terminal states stay together.
class Order(Entity):
status: OrderStatus = Lifecycle(
initial=OrderStatus.PENDING,
transitions={
OrderStatus.PENDING: [OrderStatus.PAID, OrderStatus.CANCELLED],
OrderStatus.PAID: [OrderStatus.CANCELLED],
},
terminal=[OrderStatus.CANCELLED],
)
terminal states have teeth: an entity whose lifecycle field is in a terminal state cannot be modified by any action, and a transition out of a terminal state is a structural error.
Scenario
A concrete example: initial state, one action, expected outcome.
from analint import Scenario, Expect, Assert, Emitted
sc_happy = Scenario(
name="Successful purchase",
action=checkout,
given=[
Order(id="o1", total=50.0, status=OrderStatus.PENDING, customer_id="c1"),
Wallet(balance=100.0, customer_id="c1"),
Product(stock=5, price=50.0, name="Widget"),
],
then=[
Order.status == OrderStatus.PAID,
Wallet.balance == 50.0,
Emitted(OrderPlaced),
],
expected=Expect.PASS,
)
expected=Expect.FAIL documents a blocked path: the scenario is correct when at least one rule rejects the data.
For an event-payload scenario, put the event instance in given — its payload
is then visible to pre. This checks one local transition:
Scenario(action=notify_vip, given=[OrderPlaced(order_id="o1", total=500.0), Manager(alerts=0)])
Flow
An ordered, executed journey: each action runs through
the same transition kernel — its post-state feeds the next — and checkpoints
(Assert / Emitted) interleaved in steps are checked against the state
reached so far. The first rejected action or failed checkpoint fails the flow
(and the run) with a trace; a reached state that breaks an invariant fails it too.
from analint import Flow, Assert, Emitted
purchase_flow = Flow(
given=[Cart(items=0), Wallet(balance=100)],
steps=[
add_item,
Assert(Cart.items == 1),
checkout,
Assert(Order.status == OrderStatus.PAID),
Emitted(OrderPaid),
],
description="Full customer purchase journey",
)
given= is the initial state — a partial snapshot (the same one a scenario
uses): only the listed entities are present, unspecified Scope slots are
absent, and a step that needs an unlisted entity is rejected. It is required;
given=[] means an empty world and is useful only when the flow creates
everything it needs.
Reachability queries
Scenarios check states you thought of. Queries explore every reachable state (BFS over the model) and answer questions you can't answer by hand — each verdict comes with a trace:
from analint import Reachable, Unreachable, AlwaysHolds, NoDeadEnd, DeadActions
bridge_is_reachable = Reachable(Quest.bridge_crossed)
no_softlock = NoDeadEnd(goal=Quest.bridge_crossed)
hp_never_negative = AlwaysHolds(Hero.hp >= 0)
no_gold_from_air = Unreachable(Hero.gold > 6) # regression guard
every_action_used = DeadActions()
QUERIES
PASS bridge_is_reachable (Reachable, 9 states)
↳ reachable: buy_sword → fight_troll → cross_bridge
FAIL no_softlock (NoDeadEnd, 9 states)
↳ dead end: after buy_potion the goal can no longer be reached
FAIL hp_never_negative (AlwaysHolds, 9 states)
↳ breaks: buy_sword → fight_troll → cross_bridge ⇒ Hero.hp=-1
The softlock above is invisible to every scenario in the spec — nobody writes a test for a situation they didn't think of. The explorer finds it in milliseconds with the shortest trace.
- The initial state is built from entity field defaults;
given=[...]supplies or overrides instances.given_any=[[...], [...]]declares an explicit finite set of roots. initial=Initial(vary=[...], where=[...])declares an initial relation.varyfields range overbool,Enum, bounded integerField, orField(values=[...])domains; ordinary predicates filter the Cartesian product.BoundFieldvaries a field across an entireScope.- Queries quantify over every admissible root, and traces name the originating
configuration (
init #2 ⊢ …). - Numeric
Field(ge=..., le=...)constraints keep the state space finite. Driving a field out of range is an error with a trace;saturate=Trueclamps instead, for counters where only thresholds matter. - If the state space exceeds
max_states(default 10 000), the query reports INCONCLUSIVE instead of pretending. - During exploration the engine also reports violated invariants and undeclared lifecycle transitions (an effect performing
A → Cwhen the lifecycle only allowsA → B). - An ad-hoc query without editing the spec: put it in a file and run
analint check . --what-if query.py.
from analint import Bound, Count, Initial
player = Bound("player", players)
role_assignments = Initial(
vary=[player.role],
where=[Count(player, player.role == Role.MAFIA) == 1],
)
mafia_can_win = Reachable(Game.winner == Role.MAFIA, initial=role_assignments)
Spec
The root aggregate — usually just metadata:
from analint import Spec
spec = Spec(id="ecommerce", name="E-commerce Platform")
Everything else is discovered from the modules your entry point imports. Explicit lists (entities=[...], actions=[...]) are supported when precision matters; a non-empty list is used as-is.
Composition
Reusable model fragments expose an explicit Contract; one root Spec
imports those contracts:
# payments.py
from analint import Contract
payments_api = Contract(
id="payments",
version="1.0.0",
entities=[Payment],
events=[PaymentCaptured],
invariants=[payment_amount_is_positive],
actions=[capture_payment],
)
# spec.py
from analint import Spec
from .payments import payments_api
spec = Spec(
id="checkout",
name="Checkout",
imports=[payments_api],
scenarios=[capture_payment_happy],
)
Composition is deliberately explicit: when imports= is present,
auto-discovery is disabled for the root. Only contract contents and objects
listed directly on Spec are included, so private implementation actions do
not leak through Python's import graph. Duplicate ids and incomplete contract
surfaces are reported by structural validation. Multiple Spec objects in one
import graph are a load error rather than being merged implicitly.
analint show contract payments -p . reports the exact exported surface.
--what-if still adds hypothesis objects on top of the composed model.
Project layout
The spec is loaded through a single entry point — spec.py — and its import graph defines what is in the spec:
spec.py ← single file is enough for small projects
# or, multi-file (use relative imports):
myproject/
__init__.py
entities.py ← Entity subclasses + enums
events.py ← Event subclasses
invariants.py ← Invariant instances + reusable predicates
actions.py ← Action instances
flows.py ← Flow instances
scenarios.py ← Scenario instances
spec.py ← imports the tops of the graph + Spec(id=..., name=...)
# myproject/spec.py
from analint import Spec
from . import flows, scenarios # noqa: F401
spec = Spec(id="myproject", name="My Project")
A .py file in the directory that is not reachable from the entry point produces a warning — a forgotten import never silently shrinks the model.
CLI
analint check [PATH] # validate: structural checks + scenario runs
-f, --format terminal|json
-s, --scenario ID -t, --tag TAG
--strict # warnings become errors
--what-if FILE.py # add the file's objects to the model for this
# run only — test a hypothesis without editing the spec
analint show [KIND] [NAME] -p PATH # inspect the model (JSON output)
analint show -p . # overview: all ids by kind
analint show contract payments -p . # exact imported contract surface
analint show action checkout -p . # pre/effect/post/emits/scenarios of one action
analint show lifecycle Order.status -p . # transitions, terminal, unreachable states
analint affects TARGET -p PATH # impact analysis before changing something (JSON)
analint affects Wallet.balance -p . # who reads/writes the field, invariants, lifecycles
analint affects checkout -p . # what the action touches + event-linked actions
analint PATH # shorthand for `analint check PATH`
Exit codes: 0 ok · 1 findings (errors, failed scenarios, warnings with --strict) · 2 usage error · 3 spec could not be loaded · 4 inconclusive (a query exhausted its exploration budget without a verdict). JSON output carries a three-valued verdict (PASS/FAIL/INCONCLUSIVE); passed is true only on an effective PASS and reflects --strict.
What-if: check a hypothesis without touching the spec
A patch is a standalone file whose objects are added on top of the model for one
run. It must reference the spec's objects to build predicates over them; the
loaded spec's entry module is always importable under the stable alias
analint_spec, regardless of whether the spec is a package or a single file:
# /tmp/hypothesis.py
from analint import Invariant
from analint_spec import Board # the loaded spec, by stable alias
max_two = Invariant(Board.card_count <= 2, label="At most 2 cards per board")
analint check . --what-if /tmp/hypothesis.py
FAIL archive-card/happy
↳ INVARIANT failed: At most 2 cards per board
(A packaged spec can also be imported by its real package name, e.g.
from myproject.entities import Board, but analint_spec works for every
layout, including single-file specs.)
MCP server (for AI agents)
pip install analint[mcp]
analint-mcp # stdio MCP server: check, show, affects, explore, trace
The same five operations as the CLI, callable as agent tools — an agent can inspect the model, run impact analysis before a change, test a hypothesis with what_if, explore reachable states, inspect traces, and validate after editing.
What the linter checks
Structural
- Missing/duplicate ids; duplicate class names from double imports
- Predicates reference registered entities/events and existing fields
- Event payload bindings: fields exist, annotation types match
- Two effects on the same field (simultaneity violation)
- Field constraints on construction and after effects
- Transitions out of
terminalstates - Scenario
givencovers the entities the action references (warning) - Scenario
givenstates reachable from the lifecycle initial state (warning) - Actions without scenarios (warning); files not imported by the entry point (warning)
Scenario execution
- Invariants and pre — checked against
given - Terminal guard — an entity in a terminal lifecycle state must not be modified
- Effects applied simultaneously → post-state
- post, invariants, and field constraints — checked against the post-state
- then (
Assert,Emitted) — checked against the post-state
If expected=Expect.FAIL: the scenario passes when steps 1–2 block the action.
Reachability (queries)
BFS over all reachable states; every query answers with a trace of action ids:
Reachable(p)— a witness path exists / FAIL with "explored all N states"Unreachable(p)— regression guard; FAIL shows the counterexample pathAlwaysHolds(p)— invariant over the whole space, not just scenariosNoDeadEnd(goal)— softlock detector: a reachable state from which the goal is goneDeadActions()— actions never enabled in any reachable state- en route: invariant violations and undeclared lifecycle transitions, with traces
Examples
| Example | What it shows |
|---|---|
examples/ecommerce/ |
Single file: invariants, pre/post, effects, payload-bound events, Reachable with given |
examples/taskboard/ |
Multi-file spec: 7 entities, 8 actions, 16 scenarios, lifecycles with terminal states, event-driven actions |
examples/cloak/ |
A text-adventure game as a verifiable spec: the engine finds the walkthrough (Reachable(WON)), proves you can't get stuck (NoDeadEnd) |
examples/trollbridge/ |
Deliberately broken: all scenarios green, but the engine finds an economy softlock and an unmodelled death — bugs example-based testing cannot see |
examples/fulfillment/ |
An order-fulfillment saga as a pure domain model: 16 actions with compensations for every failure; NoDeadEnd proves no reachable state is a dead end — a clean settlement stays reachable from every state (recoverability, not a guarantee that every run finishes) |
examples/coin/ |
A line-by-line translation of Quint's flagship tutorial (the Solidity subcurrency) — reproduces the supply-overflow violation from the Quint lesson with a trace; see research/15 for the honest comparison |
examples/branch_protection/ |
First external evidence model: GitHub protected-branch PR policy, proven unbypassable across every action order; measured requirement-change series (research/23) |
examples/oauth/ |
Second external evidence model: OAuth 2.0 auth-code + PKCE — two clients, relational token provenance, replay revocation; explicit multi-file Contract split + a Quint port (research/24) |
examples/mafia/ |
Mafia/Werewolf from Quint: the citizens cannot win under every nondeterministic role assignment (declarative Initial, role-generic Param actions; research/16) |
examples/sunless_crypt/ |
A dungeon crawl that is both checked and played: the same spec is verified by analint check and run as a text game by examples/play.py (research/21) |
examples/k8s_replicaset/ |
The project-sized dogfood: a Kubernetes ReplicaSet reconciling Pods under a count/pods ResourceQuota — multiplicity + presence + Count + ownerReference provenance, reachability/safety only (liveness deliberately out of scope; research/26 §P4.5) |
Status
This is the first public release (0.0.1). The engine and CLI are mature and
covered by 350+ tests, but the API is not yet frozen and may change before 1.0 —
pin a version if you depend on it.
analint performs bounded reachability over a finite state graph. It checks
safety and reachability — invariants, Reachable, NoDeadEnd, dead actions — and
returns an honest completeness verdict, preferring INCONCLUSIVE / NOT_CHECKED
over a silent pass. It deliberately does not model liveness or temporal
"eventually" properties; that is a scope boundary, not a defect.
Contributing
See CONTRIBUTING.md for development setup, running the tests, and the review-gated workflow.
License
MIT © angru
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file analint-0.0.1.tar.gz.
File metadata
- Download URL: analint-0.0.1.tar.gz
- Upload date:
- Size: 179.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d221a404a3a9c4efa791ecf57937fd03839fa0398564c3875c9a1b04c6adfb48
|
|
| MD5 |
e3e5eaf2d0809f3280232111ee9e4b53
|
|
| BLAKE2b-256 |
6bbbf8405d902e44414a8caaae4e7155ed85c7629740d9799f16b4a99c2c2895
|
Provenance
The following attestation bundles were made for analint-0.0.1.tar.gz:
Publisher:
publish.yml on angru/analint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
analint-0.0.1.tar.gz -
Subject digest:
d221a404a3a9c4efa791ecf57937fd03839fa0398564c3875c9a1b04c6adfb48 - Sigstore transparency entry: 1895772281
- Sigstore integration time:
-
Permalink:
angru/analint@fb2bff151e71f829a325f73c40af6af984a07b7a -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/angru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb2bff151e71f829a325f73c40af6af984a07b7a -
Trigger Event:
release
-
Statement type:
File details
Details for the file analint-0.0.1-py3-none-any.whl.
File metadata
- Download URL: analint-0.0.1-py3-none-any.whl
- Upload date:
- Size: 99.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2223df6a879983108cb73ab43ebb93fef0e228ff1c3e1f0bf53ec475f67cf910
|
|
| MD5 |
9d8d98d478ebfb188ebce90ef7bc0081
|
|
| BLAKE2b-256 |
1024211eeaef7c5cec01eee9d99cc7cb888ee0db9c28571b8730a39a7a7da09d
|
Provenance
The following attestation bundles were made for analint-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on angru/analint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
analint-0.0.1-py3-none-any.whl -
Subject digest:
2223df6a879983108cb73ab43ebb93fef0e228ff1c3e1f0bf53ec475f67cf910 - Sigstore transparency entry: 1895772554
- Sigstore integration time:
-
Permalink:
angru/analint@fb2bff151e71f829a325f73c40af6af984a07b7a -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/angru
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fb2bff151e71f829a325f73c40af6af984a07b7a -
Trigger Event:
release
-
Statement type: