A production-quality Open FAIR library for quantitative cyber risk analysis
Project description
pyCRQ
A Python library for quantitative cyber risk analysis using the Open FAIR standard. Model threats as probability distributions, run Monte Carlo simulations, and get dollar-denominated risk estimates with full uncertainty ranges.
Table of Contents
- How FAIR Works
- Installation
- Your First Scenario in 5 Minutes
- Core Concepts
- ScenarioBuilder Reference
- Running Simulations
- Analysis & Reporting
- Common Recipes
- API Reference
- Examples
How FAIR Works
FAIR (Factor Analysis of Information Risk) breaks down cyber risk into two questions:
- How often will a loss event occur? → Loss Event Frequency (LEF)
- How much will it cost when it does? → Loss Magnitude (LM)
Multiply those together and you get Annual Loss Exposure (ALE) — the expected annual cost of the risk in dollars.
ALE = Loss Event Frequency × Loss Magnitude
FAIR further decomposes each side of that equation:
┌─────────────────────────────────────────────┐
│ FAIR Risk Model │
└─────────────────────────────────────────────┘
Annual Loss Exposure (ALE)
│
┌────────────┴───────────────┐
│ │
Loss Event Frequency Loss Magnitude (LM)
(LEF) │
│ ┌────────┴────────────┐
┌─────┴──────┐ Primary Loss Secondary Loss
│ │ │ │
TEF Vuln (Productivity, Response, (SLEF × SLM)
│ │ Replacement, Competitive
┌─┴──┐ ┌──┴──┐ Advantage, Fines, Reputation)
CF PoA TCap CS
| Term | Full Name | What It Means |
|---|---|---|
| LEF | Loss Event Frequency | How many loss events occur per year |
| TEF | Threat Event Frequency | How often the threat attempts an action |
| CF | Contact Frequency | How often the threat contacts the asset |
| PoA | Probability of Action | Given contact, how likely the threat acts |
| Vuln | Vulnerability | Probability a threat event becomes a loss event |
| TCap | Threat Capability | Attacker skill (0–100 score) |
| CS | Control Strength | Defensive strength (0–100 score) |
| LM | Loss Magnitude | Financial cost per loss event |
| ALE | Annual Loss Exposure | Expected annual cost = LEF × LM |
You don't need all of these. Most scenarios only need 3–4 inputs. See Frequency Inputs for how to choose.
Installation
pip install pycrq
From source:
git clone https://github.com/securemetrics/pyCRQ.git
cd pyCRQ
pip install -e .
Requirements: Python 3.9+, numpy, scipy, matplotlib (all installed automatically).
Your First Scenario in 5 Minutes
Here is a complete working example. Copy and run it.
from pycrq import ScenarioBuilder, FAIRSimulator, pert, scenario_report
# Step 1: Describe the scenario
scenario = (
ScenarioBuilder("Ransomware Attack")
.asset("Customer Database", "Information")
.threat("External Hacker", "External Adversarial")
# Step 2: How often? (TEF = attempts per year, Vuln = % of attempts that succeed)
.tef(pert(low=1, mode=3, high=8)) # 1 to 8 attacks per year
.vulnerability(pert(low=0.10, mode=0.25, high=0.55)) # 10–55% success rate
# Step 3: How much does it cost per event?
.primary_loss(pert(low=50_000, mode=250_000, high=1_500_000))
.build()
)
# Step 4: Simulate
simulator = FAIRSimulator(n_simulations=10_000, seed=42)
result = simulator.simulate(scenario)
# Step 5: Read the results
print(scenario_report(result))
print(f"\nMean ALE: ${result.mean_ale:,.0f}")
print(f"90th pct: ${result.percentile(90):,.0f}")
print(f"VaR (95%): ${result.var(0.95):,.0f}")
Output looks like:
============================================================
FAIR Simulation Report — Ransomware Attack
============================================================
Simulations: 10,000
Mean ALE: $245,312
Median ALE: $142,891
P90 ALE: $621,440
VaR (95%): $788,230
CVaR (95%): $1,104,560
Risk Level: HIGH
============================================================
That's it. The rest of this guide explains how to build more detailed and accurate models.
Core Concepts
Distributions — Expressing Uncertainty
In FAIR, every input is a range, not a single number. You express uncertainty using probability distributions. The most common one is pert().
The PERT Distribution (Start Here)
pert(low, mode, high) is perfect for expert estimates: "I think it's usually around X, but could be as low as Y or as high as Z."
from pycrq import pert
# Threat event frequency: usually 3/year, but anywhere from 1 to 8
tef = pert(low=1, mode=3, high=8)
# Vulnerability: usually 25%, but could be 10% to 55%
vuln = pert(low=0.10, mode=0.25, high=0.55)
# Loss magnitude: usually $250K, but could be $50K to $1.5M
loss = pert(low=50_000, mode=250_000, high=1_500_000)
All Available Distributions
| Distribution | Function | Best For |
|---|---|---|
| PERT | pert(low, mode, high) |
Expert elicitation — "optimistic / most-likely / pessimistic" |
| Triangular | triangular(low, mode, high) |
Like PERT but with sharper edges; use when the tails matter more |
| Uniform | uniform(low, high) |
Complete uncertainty between two bounds |
| Normal | normal(mean, std) |
Symmetric uncertainty around a known mean |
| LogNormal | lognormal(meanlog, sdlog) |
Right-skewed costs and frequencies (never goes negative) |
| Beta | beta(alpha, beta) |
Probabilities (0–1); flexible shape |
| Poisson | poisson(lam) |
Discrete event counts when you know the expected annual rate |
| Constant | constant(value) |
Fix a factor to a known value (e.g., during sensitivity analysis) |
from pycrq import pert, normal, lognormal, uniform, triangular, constant, beta, poisson
from pycrq import LogNormalDistribution
# PERT — workhorse for expert estimates
freq = pert(1, 3, 8)
# Normal — symmetric; good for aggregated costs
cost = normal(mean=500_000, std=100_000)
# LogNormal — right-skewed; great for breach costs that can spike high
cost = LogNormalDistribution.from_moments(mean=500_000, std=300_000)
# Uniform — "anywhere in this range is equally likely"
vuln = uniform(low=0.1, high=0.4)
# Triangular — sharper peak than PERT
freq = triangular(low=1, mode=4, high=12)
# Constant — pin a factor to an exact value
exact = constant(value=50_000)
# Beta — vulnerability as a 0–1 probability with flexible skew
vuln = beta(alpha=2, beta=5) # skewed toward low values
# Poisson — discrete event counts; mean=3.5 events/year
freq = poisson(lam=3.5)
Quick tip: All distributions expose .mean(), .std(), .percentile(p), and .sample(n) so you can inspect them before simulating:
d = pert(1, 3, 8)
print(d.mean()) # 3.0
print(d.percentile(90)) # ~5.8
print(d.sample(5)) # array([2.1, 4.7, 3.3, 1.8, 6.2])
Frequency Inputs — How Often Does It Happen?
This is the most flexible part of the model. You can provide frequency at three different levels of detail. Pick whichever fits your data.
Option A — Direct LEF (simplest)
Use .lef() when you have a single estimate of how many loss events occur per year — you've already folded in the probability of success.
scenario = (
ScenarioBuilder("Phishing Campaign")
.asset("Email System", "System")
.threat("Phishing Group", "External Adversarial")
.lef(pert(0.5, 1.5, 4)) # 0.5 to 4 successful phishing incidents/year
.primary_loss(pert(10_000, 80_000, 400_000))
.build()
)
⚠️ Trade-off:
.lef()is quick to model, but bypasses TEF and Vulnerability entirely, so per-factor sensitivity analysis (e.g., "what if we improved our controls?") is unavailable. Use.tef()+.vulnerability()when you want to model control effectiveness.
Option B — TEF + Vulnerability (recommended)
This is the standard FAIR approach. You separately model how often attacks are attempted and how often they succeed.
scenario = (
ScenarioBuilder("Credential Stuffing")
.asset("Login Portal", "System")
.threat("Bot Network", "External Adversarial")
.tef(pert(50, 200, 1000)) # 50–1,000 attempts per year
.vulnerability(pert(0.01, 0.05, 0.15)) # 1–15% succeed
.primary_loss(pert(5_000, 30_000, 200_000))
.build()
)
Option C — Full Decomposition (most detail)
Break TEF down into Contact Frequency × Probability of Action, and model Vulnerability from Threat Capability vs. Control Strength scores (0–100). Best for comparing control investment scenarios.
scenario = (
ScenarioBuilder("Insider Threat")
.asset("Financial Records", "Information")
.threat("Malicious Insider", "Internal Adversarial")
.contact_frequency(pert(10, 30, 60)) # contacts/year
.probability_of_action(pert(0.05, 0.15, 0.30)) # acts on 5–30% of contacts
.threat_capability(pert(30, 55, 75)) # attacker skill score
.control_strength(pert(50, 65, 80)) # control effectiveness score
.primary_loss(pert(100_000, 400_000, 2_000_000))
.build()
)
When both threat_capability and control_strength are provided, vulnerability is computed via a logistic function:
Vuln = sigmoid(6 × (ThreatCapability − ControlStrength) / 100)
This means if your controls are stronger than the threat (CS > TCap), vulnerability drops sharply — and vice versa.
Priority Order
The simulator uses the first complete input it finds:
LEF → TEF → CF × PoA → CF alone
For vulnerability:
direct vulnerability → TCap + CS (logistic) → TCap alone → CS alone → Uniform(0,1)
Loss Inputs — How Much Does It Cost?
Option A — Single primary loss
The simplest approach — one distribution covers the total cost per event:
.primary_loss(pert(50_000, 250_000, 1_500_000))
Option B — By loss category
Break loss into FAIR's six primary categories. These are summed automatically:
.loss_productivity(pert(50_000, 200_000, 750_000)) # Revenue/output lost
.loss_response(pert(30_000, 100_000, 400_000)) # IR, forensics, legal
.loss_replacement(pert(10_000, 50_000, 200_000)) # Hardware/software replacement
.loss_competitive_advantage(pert(0, 25_000, 150_000)) # Market position impact
.loss_fines_judgments(pert(0, 75_000, 500_000)) # Regulatory fines
.loss_reputation(pert(0, 50_000, 300_000)) # Brand damage
Adding Secondary Loss
Secondary loss models costs triggered by external parties reacting to the event (e.g., regulators, customers, press):
.secondary_loss(
slef=pert(0.10, 0.25, 0.50), # probability secondary event occurs
slm=pert(100_000, 500_000, 3_000_000) # cost when it does
)
ScenarioBuilder Reference
All methods return self for chaining. Call .build() at the end.
Asset & Threat
| Method | Description |
|---|---|
.asset(name, asset_type, description) |
Define the asset at risk. asset_type: "Information", "System", "Service", "Physical", "Person", "Other" |
.with_asset(asset_obj) |
Attach a pre-built Asset object |
.threat(name, threat_type, description) |
Define the threat agent. threat_type: "External Adversarial", "Internal Adversarial", "External Non-Adversarial", "Internal Non-Adversarial", "Natural" |
.with_threat(threat_obj) |
Attach a pre-built ThreatAgent object |
.add_control(name, description, control_type) |
Document a control (e.g., "Preventive", "Detective") |
Metadata
| Method | Description |
|---|---|
.describe(text) |
Free-text narrative for the scenario |
.tag("tag1", "tag2") |
Add string tags for filtering/grouping |
.effect("Confidentiality") |
CIA impact type: "Confidentiality", "Integrity", "Availability", "Safety" |
.time_horizon(years) |
Simulation time window in years (default 1.0) |
Frequency
| Method | Use When | Notes |
|---|---|---|
.lef(dist) |
You have a direct estimate of annual loss events | Bypasses TEF/Vuln; no per-factor sensitivity analysis |
.tef(dist) |
You know how often attacks are attempted | Combine with .vulnerability() |
.contact_frequency(dist) |
You know how often the threat encounters the asset | Multiply by .probability_of_action() to get TEF |
.probability_of_action(dist) |
Given contact, probability the threat acts | Used with .contact_frequency() |
.threat_capability(dist) |
Attacker skill as a 0–100 score | Combined with .control_strength() via logistic function |
.control_strength(dist) |
Control effectiveness as a 0–100 score | Combined with .threat_capability() via logistic function |
.vulnerability(dist) |
Direct probability (0–1) that an attempt succeeds | Simpler than TCap/CS; use when you can estimate it directly |
Loss Magnitude
| Method | Description |
|---|---|
.primary_loss(dist) |
Total primary loss per event (alternative to individual categories) |
.loss_productivity(dist) |
Revenue or output lost |
.loss_response(dist) |
Incident response, forensics, legal costs |
.loss_replacement(dist) |
Asset repair or replacement costs |
.loss_competitive_advantage(dist) |
Impact on market position |
.loss_fines_judgments(dist) |
Regulatory fines and legal judgments |
.loss_reputation(dist) |
Brand and reputation damage |
.secondary_loss(slef, slm) |
Secondary loss event frequency + magnitude |
Running Simulations
from pycrq import FAIRSimulator
simulator = FAIRSimulator(
n_simulations=10_000, # more = more stable results; 10K is a good default
seed=42, # set for reproducibility; omit for random results
)
# Single scenario
result = simulator.simulate(scenario)
# Entire risk register
agg = simulator.simulate_register(register)
Reading Results
# Key statistics
result.mean_ale # mean annual loss exposure
result.std_ale # standard deviation
result.percentile(10) # 10th percentile ALE
result.percentile(50) # median ALE
result.percentile(90) # 90th percentile ALE
result.var(0.95) # Value at Risk (95%)
result.cvar(0.95) # Conditional VaR / Expected Shortfall (95%)
# Raw arrays (one value per simulation)
result.annual_loss_exposure # np.ndarray, shape (n_simulations,)
result.loss_event_frequency # np.ndarray
result.loss_magnitude # np.ndarray
result.threat_event_frequency # np.ndarray (None if .lef() was used)
result.vulnerability # np.ndarray (None if .lef() was used)
# Full summary dict
result.summary()
Analysis & Reporting
Print a Report
from pycrq import scenario_report, register_report
print(scenario_report(result)) # single scenario
print(scenario_report(result, verbose=True)) # includes per-category breakdown
print(register_report(agg)) # full risk register
Compute Metrics
from pycrq import compute_metrics, classify_risk_level, format_currency
metrics = compute_metrics(result)
print(metrics.mean_ale)
print(metrics.var_95)
print(metrics.risk_level) # "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"
# Classify any dollar value
level = classify_risk_level(250_000) # "MEDIUM"
# Format numbers for display
format_currency(1_500_000) # "$1.50M"
format_currency(85_000) # "$85.0K"
Compare Scenarios Side-by-Side
from pycrq import rank_scenarios, summary_table
# Rank by mean ALE (highest first)
ranked = rank_scenarios(results, by="mean_ale")
for name, value in ranked:
print(f"{name}: {format_currency(value)}")
# ASCII comparison table
print(summary_table(results))
Validate Before You Simulate
from pycrq import validate_scenario
warnings = validate_scenario(scenario)
for w in warnings:
print(f"[WARN] {w}")
This catches common issues like missing frequency inputs, unrealistic vulnerability values, and missing secondary loss definitions.
Plots
from pycrq import plot_ale_distribution, plot_scenario_comparison, plot_risk_matrix
# ALE histogram with percentile markers
fig = plot_ale_distribution(result, title="Ransomware ALE", show_percentiles=True)
fig.savefig("ale_distribution.png", dpi=150)
# Bar chart comparing multiple scenarios
fig = plot_scenario_comparison(results, metric="mean_ale")
# Risk matrix
fig = plot_risk_matrix(results)
Common Recipes
Risk Register (Multiple Scenarios)
Model an entire threat landscape and analyze the portfolio.
from pycrq import RiskRegister, FAIRSimulator, register_report, rank_scenarios
register = RiskRegister(name="Enterprise Cyber 2026")
register.add_scenario(
ScenarioBuilder("Ransomware")
.asset("File Servers", "System")
.threat("Ransomware Gang", "External Adversarial")
.tef(pert(0.5, 1, 3))
.vulnerability(pert(0.2, 0.4, 0.7))
.primary_loss(pert(200_000, 800_000, 5_000_000))
.build()
)
register.add_scenario(
ScenarioBuilder("Phishing — Credential Theft")
.asset("Email / Identity", "Information")
.threat("Phishing Group", "External Adversarial")
.tef(pert(5, 15, 40))
.vulnerability(pert(0.02, 0.05, 0.12))
.primary_loss(pert(10_000, 60_000, 300_000))
.build()
)
register.add_scenario(
ScenarioBuilder("Insider Data Theft")
.asset("Customer Records", "Information")
.threat("Malicious Employee", "Internal Adversarial")
.lef(pert(0.1, 0.3, 1)) # historical rate — skip TEF/Vuln decomposition
.primary_loss(pert(50_000, 250_000, 1_000_000))
.secondary_loss(
slef=pert(0.2, 0.4, 0.7),
slm=pert(100_000, 500_000, 2_000_000)
)
.build()
)
simulator = FAIRSimulator(n_simulations=10_000, seed=42)
agg = simulator.simulate_register(register)
print(register_report(agg))
# Rank scenarios by mean ALE
from pycrq import rank_scenarios
ranked = rank_scenarios(agg.scenario_results, by="mean_ale")
for name, value in ranked:
print(f" {name}: ${value:,.0f}")
Control ROI / ROSI
Compare risk before and after a control, then calculate return on security investment.
from pycrq import ScenarioBuilder, FAIRSimulator, control_roi, compute_risk_reduction, pert
simulator = FAIRSimulator(n_simulations=10_000, seed=42)
# Before: no MFA
before = (
ScenarioBuilder("Account Takeover — No MFA")
.asset("Customer Portal", "System")
.threat("Credential Stuffers", "External Adversarial")
.tef(pert(20, 60, 200))
.vulnerability(pert(0.05, 0.15, 0.35))
.primary_loss(pert(5_000, 40_000, 200_000))
.build()
)
# After: MFA deployed — stronger controls, lower vulnerability
after = (
ScenarioBuilder("Account Takeover — With MFA")
.asset("Customer Portal", "System")
.threat("Credential Stuffers", "External Adversarial")
.tef(pert(20, 60, 200))
.vulnerability(pert(0.01, 0.03, 0.08)) # MFA cuts success rate dramatically
.primary_loss(pert(5_000, 40_000, 200_000))
.build()
)
before_result = simulator.simulate(before)
after_result = simulator.simulate(after)
# Risk reduction
reduction = compute_risk_reduction(before_result, after_result)
print(f"ALE reduction: ${reduction['mean_ale_reduction_abs']:,.0f}")
print(f"Reduction %: {reduction['mean_ale_reduction_pct']:.1f}%")
# ROSI — MFA costs $60K/year to operate
roi = control_roi(before_result, after_result, control_cost=60_000)
print(f"ROSI: {roi['rosi_pct']:.0f}%")
print(f"Net benefit: ${roi['net_benefit']:,.0f}")
print(f"Break-even: {roi['break_even_years']:.1f} years")
print(f"Recommended: {roi['recommended']}")
Sensitivity Analysis
Sweep a parameter across a range to see how much it drives your ALE. Useful for prioritizing control investments.
from pycrq import ScenarioBuilder, FAIRSimulator, sensitivity_analysis, pert, constant
def build_scenario(control_strength_value):
return (
ScenarioBuilder("Phishing — Control Sweep")
.asset("Email", "Information")
.threat("Phisher", "External Adversarial")
.contact_frequency(pert(10, 30, 80))
.probability_of_action(pert(0.2, 0.4, 0.7))
.threat_capability(pert(40, 60, 80))
.control_strength(constant(control_strength_value)) # sweep this
.primary_loss(pert(10_000, 75_000, 400_000))
.build()
)
results = sensitivity_analysis(
scenario_factory=build_scenario,
param_name="control_strength",
param_values=[20, 40, 60, 80, 95],
n_simulations=5_000,
seed=42,
)
for r in results:
cs = r.scenario_name.split("=")[-1]
print(f"Control Strength {cs}: Mean ALE = ${r.mean_ale:,.0f}")
Bootstrap Confidence Intervals
Quantify how much uncertainty exists in your simulation estimates themselves.
from pycrq import bootstrap_confidence_interval
lower, upper = bootstrap_confidence_interval(
result,
statistic="mean", # or "p90", "var_95"
confidence=0.95,
n_bootstrap=1_000,
)
print(f"Mean ALE 95% CI: ${lower:,.0f} – ${upper:,.0f}")
Calibrate from Expert P10/P90
If a SME gives you confidence bounds rather than a three-point estimate, use calibrate_pert_from_confidence to convert them:
from pycrq import calibrate_pert_from_confidence, pert
# "I'm 90% sure the loss is between $50K and $2M"
low, mode, high = calibrate_pert_from_confidence(p10=50_000, p90=2_000_000)
loss_dist = pert(low, mode, high)
Export to JSON or CSV
from pycrq import export_json, export_csv
export_json(result, "outputs/ransomware.json")
export_csv(result, "outputs/ransomware.csv")
# The JSON captures all metadata and is round-trippable
API Reference
Distributions
| Function | Parameters | Notes |
|---|---|---|
pert(low, mode, high, lam=4.0) |
low ≤ mode ≤ high | Best default choice for expert estimates |
normal(mean, std) |
std ≥ 0 | Symmetric; can go negative — use for aggregated costs |
lognormal(meanlog, sdlog) |
sdlog > 0 | Log-space params; use from_moments() for real-space |
uniform(low, high) |
high ≥ low | Flat uncertainty |
triangular(low, mode, high) |
low ≤ mode ≤ high | Like PERT with less weight on mode |
constant(value) |
any float | Pins a factor to an exact value |
beta(alpha, beta, low=0, high=1) |
alpha, beta > 0 | Flexible 0–1 probability shape |
poisson(lam) |
lam > 0 | Discrete counts; mean = variance = lam |
from_dict(d) |
dict with "type" key |
Deserialize any distribution from its to_dict() output |
LogNormalDistribution.from_moments(mean, std) |
mean, std > 0 | Construct from real-space moments |
FAIRSimulator
FAIRSimulator(n_simulations=10_000, seed=None)
.simulate(scenario) # → SimulationResult
.simulate_register(register) # → AggregateSimulationResult
SimulationResult:
| Attribute / Method | Type | Description |
|---|---|---|
annual_loss_exposure |
np.ndarray |
ALE per simulation |
loss_event_frequency |
np.ndarray |
LEF per simulation |
loss_magnitude |
np.ndarray |
LM per simulation |
threat_event_frequency |
np.ndarray | None |
TEF (None when .lef() path used) |
vulnerability |
np.ndarray | None |
Vuln (None when .lef() path used) |
primary_loss |
np.ndarray |
Primary loss component |
secondary_loss |
np.ndarray | None |
Secondary loss (None if not modelled) |
loss_by_category |
dict |
Per-category arrays when using category methods |
mean_ale |
float |
Mean ALE |
std_ale |
float |
Standard deviation of ALE |
.percentile(p) |
float |
p-th percentile of ALE (0–100) |
.var(confidence) |
float |
Value-at-Risk |
.cvar(confidence) |
float |
Conditional VaR / Expected Shortfall |
.summary() |
dict |
All key statistics as a dict |
Analysis Functions
from pycrq import (
compute_metrics, # → RiskMetrics from SimulationResult
classify_risk_level, # classify_risk_level(mean_ale) → "LOW"|"MEDIUM"|"HIGH"|"CRITICAL"
rank_scenarios, # rank_scenarios(results, by="mean_ale") → [(name, value), ...]
compute_risk_reduction, # compute_risk_reduction(before, after) → dict
control_roi, # control_roi(before, after, control_cost) → dict
sensitivity_analysis, # sensitivity_analysis(factory_fn, param_name, values, ...) → list
bootstrap_confidence_interval, # (result, statistic, confidence, n_bootstrap) → (lower, upper)
)
Utilities
from pycrq import (
validate_scenario, # → List[str] of advisory warnings
calibrate_pert_from_confidence, # (p10, p90) → (low, mode, high)
frequency_to_rate, # "monthly" → 12.0, "weekly" → 52.0, etc.
annualize, # annualize(5_000, "monthly") → 60_000
summary_table, # summary_table(results) → ASCII str
)
Examples
Ready-to-run examples are in the examples/ directory:
| File | What It Shows |
|---|---|
examples/01_basic_scenario.py |
Full ransomware scenario — PERT distributions, full report, bootstrap CI |
examples/02_risk_register.py |
5-scenario risk register, aggregate portfolio, rankings |
examples/03_control_roi.py |
Before/after MFA comparison, ROSI, break-even analysis |
examples/04_sensitivity_analysis.py |
Threat capability sweep 20→80, control strength sweep, logistic vulnerability |
python examples/01_basic_scenario.py
python examples/02_risk_register.py
python examples/03_control_roi.py
python examples/04_sensitivity_analysis.py
License
MIT License. See LICENSE for details.
Open FAIR is a standard published by The Open Group. This library is an independent implementation and is not affiliated with or endorsed by The Open Group.
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 pycrq-1.1.0.tar.gz.
File metadata
- Download URL: pycrq-1.1.0.tar.gz
- Upload date:
- Size: 49.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a8739afaad33e491ea6ddf5bd51df4299654d34367a2b0fd4b969cbdca9263a
|
|
| MD5 |
f78d0ec5dda7ab00a5753610b378a7d0
|
|
| BLAKE2b-256 |
caed099d3379ccefe3e6121f059e361db2e0a611e4f1b0b9c145a75409206c64
|
File details
Details for the file pycrq-1.1.0-py3-none-any.whl.
File metadata
- Download URL: pycrq-1.1.0-py3-none-any.whl
- Upload date:
- Size: 41.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4ef5de891a35df24569f0dba0de9a4d4395431da667be2d4c5fc0246a7b7a3e5
|
|
| MD5 |
0c59566c76d8713f92985a9ad10b2270
|
|
| BLAKE2b-256 |
a487255c15368f4f34918d7ff47b7a23abf983b7d4a74c9541d24bf309a76914
|