Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
22d5510
feat: list the SQBRPYTHON rules over Fensu's core Python rules
kvlonge Nov 14, 2026
b862b6f
feat: declare Fensu's thresholds as whole-number options on the Pytho…
kvlonge Nov 14, 2026
09904ac
feat: put the project's Python sources into rules requests that selec…
kvlonge Nov 14, 2026
6d1f1d3
build: record the rules crate's dependency on discovery in the lockfile
kvlonge Nov 14, 2026
b5b7d25
feat: evaluate the SQBRPYTHON rules over project Python with Fensu's …
kvlonge Nov 14, 2026
76af008
feat: check what sits directly under python/ and how its packages are…
kvlonge Nov 14, 2026
86fe6d2
feat: reuse each Python file's findings while its file, rules and pro…
kvlonge Nov 14, 2026
dab310c
test: require a failure-corpus case for every discovery error code in…
kvlonge Nov 14, 2026
15be26f
test: require a golden for every failure-corpus case in unit tests
kvlonge Nov 14, 2026
ca6d499
perf: give the dead-code context only to the call that runs unreachab…
kvlonge Nov 14, 2026
f46b697
test: check the Python rules through compile and rules run on DuckSho…
kvlonge Nov 14, 2026
b459e2c
docs: document the SQBRPYTHON rules and how SQBR leaves them out
kvlonge Nov 14, 2026
7b2882d
fix: select only named Python rules and key their cache on their own …
kvlonge Nov 14, 2026
9e9d7ad
fix: treat audit factories and namespace packages as reachable in SQB…
kvlonge Nov 14, 2026
28adadd
fix: compare Python rule query paths with forward slashes so Windows …
kvlonge Nov 14, 2026
f2a034e
style: sort imports in the conforming Python rules fixture
kvlonge Nov 14, 2026
3449112
test: allow cases without an error code in the discovery-codes check
kvlonge Nov 14, 2026
3b2892d
refactor: name the iterator steps in the Python rule cache and namesp…
kvlonge Nov 14, 2026
499e904
test: cover the dropped-stdout band with build help now that rules li…
kvlonge Nov 14, 2026
7a50a20
test: select the Python rules in the all-rules benchmark fixture
kvlonge Nov 14, 2026
ff0a401
test: guard unreachable-definition scaling by growth ratio instead of…
kvlonge Nov 14, 2026
0249aac
test: check the growth guard's large run still reports every unreacha…
kvlonge Nov 14, 2026
9e1feaf
test: keep the delta compile project's rule selection without the Pyt…
kvlonge Nov 14, 2026
9a006f9
test: drop the standalone comment from the delta series constants
kvlonge Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ axum-server = { version = "0.8", default-features = false, features = ["tls-rust
# Compresses the server's larger JSON reads and UI assets for browsers.
tower-http = { version = "0.6", default-features = false, features = ["compression-gzip"] }
fensu-policy = "=0.22.0"
# Fensu's Python facts and core rules without their Python bindings: the default `python`
# feature adds PyO3, rusqlite, flate2 and sha2.
fensu-facts = { version = "=0.30.0", default-features = false }
fensu-native = { version = "=0.30.0", default-features = false }
futures-util = { version = "0.3", default-features = false, features = ["std"] }
# `sqb` names compute log captures with random ids, as `uuid.uuid4().hex`.
getrandom = "0.4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AUDIT ();

SELECT *
FROM @relation
WHERE NOT (@expression)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Note each finished build."""


def note_build(ctx: object) -> None:
"""Note one finished build."""
print(ctx)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MODEL (description "Shipped orders.", audit_factories [shipment_quality]);
SELECT 1 AS shipment_id
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Write each shipped order as one carrier line."""

from python.utils.currency import amounts


def export_shipments(ctx: object) -> None:
"""Write each shipped order as one carrier line."""
label: str = "Expédition"
print(label, amounts.cents_text(1250), ctx)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Export shipped orders to the carrier."""

from python.fulfillment.shipment_export.main import export_shipments
from sqlbuild.tasks import task


@task
def shipment_export(ctx: object) -> None:
"""Export shipped orders to the carrier."""
export_shipments(ctx)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Audit cases every shipment table shares."""

from sqlbuild.audits import AuditCase, audit_factory


@audit_factory
def shipment_quality() -> list[AuditCase]:
"""Every shipment has a positive identifier."""
return [
AuditCase(
name="positive_shipment_id",
definition="expression_is_true",
arguments={"expression": "shipment_id > 0"},
)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Format money for carrier files."""


def cents_text(cents: int) -> str:
"""Return cents as a decimal amount."""
return f"{cents // 100}.{cents % 100:02d}"
16 changes: 16 additions & 0 deletions benchmarks/duckshop/edge_cases/python_rules/expected_findings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"select": ["SQBRPYTHON", "SQBRPYTHON306"],
"findings": [
["SQBRPYTHON101", "hooks/python/alert.py", 4, 11],
["SQBRPYTHON613", "python/inventory/__init__.py", 1, 1],
["SQBRPYTHON002", "python/inventory/common/__init__.py", 1, 1],
["SQBRPYTHON306", "python/inventory/common/stock.py", 4, 1],
["SQBRPYTHON305", "python/inventory/main/publish.py", 1, 1],
["SQBRPYTHON306", "python/inventory/main/publish.py", 4, 1],
["SQBRPYTHON617", "python/inventory/restock/main.py", 1, 1],
["SQBRPYTHON306", "python/inventory/restock/main.py", 4, 1],
["SQBRPYTHON306", "python/inventory/restock/main.py", 9, 1],
["SQBRPYTHON001", "python/report.py", 1, 1],
["SQBRPYTHON614", "python/report.py", 1, 1]
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Alert on failed builds."""


def alert(ctx, reason: str) -> None:
"""Alert on one failed build."""
print(ctx, reason)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Stock levels."""


def stock_level(sku: str) -> int:
"""Return the stock level of one product."""
return len(sku)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Publish stock levels."""


def publish_levels() -> None:
"""Publish stock levels."""
print("levels")
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Restock products."""


def restock(sku: str) -> None:
"""Restock one product."""
print(sku)


def recount(sku: str) -> None:
"""Recount one product."""
print(sku)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""A module directly under python/."""
10 changes: 9 additions & 1 deletion crates/sqlbuild-cli/src/native_commands/_helpers/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,21 @@ fn option_spec(option: &BuiltinOption) -> OptionSpec {
OptionValue::Tuple(Vec::new()),
Some(OptionKind::Other(DICT_TYPE_NAME.to_owned())),
),
BuiltinOptionKind::Integer { default, .. } => {
(OptionKind::Int, OptionValue::Int(i64::from(default)), None)
}
};
let minimum: Option<i64> = match option.kind {
BuiltinOptionKind::Integer { minimum, .. } => Some(i64::from(minimum)),
_ => None,
};
OptionSpec {
name: option.name.to_owned(),
description: option.description.to_owned(),
value_type,
default,
choices: Vec::new(),
minimum: None,
minimum,
maximum: None,
minimum_items: None,
item_type,
Expand All @@ -63,6 +70,7 @@ fn option_text(option: &BuiltinOption) -> OptionText {
BuiltinOptionKind::Boolean => FALSE_REPR.to_owned(),
BuiltinOptionKind::StringList(defaults) => string_tuple_repr(defaults),
BuiltinOptionKind::TableList => EMPTY_TUPLE_REPR.to_owned(),
BuiltinOptionKind::Integer { default, .. } => default.to_string(),
};
OptionText {
name: option.name.to_owned(),
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-discovery/src/python_paths/main/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod supported_python_paths;
pub mod unsupported_python_paths;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Project Python files under the documented executable roots.

use std::path::Path;

use crate::python_paths::_helpers::python_files::python_files;
use crate::python_paths::_helpers::supported::is_supported;

/// Each supported Python file below `project_dir` as relative parts, sorted part by part.
pub fn supported_python_paths(project_dir: &Path) -> Vec<Vec<String>> {
python_files(project_dir)
.into_iter()
.filter(|parts| is_supported(parts))
.collect()
}
2 changes: 2 additions & 0 deletions crates/sqlbuild-discovery/src/python_paths/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[path = "tests/test_supported_python_paths.rs"]
mod supported_python_paths;
#[path = "tests/test_types.rs"]
mod test_types;
#[path = "tests/test_unsupported_python_paths.rs"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::python_paths::main::supported_python_paths::supported_python_paths;
use crate::python_paths::tests::test_types::SupportedPathsTestCase;

#[test]
fn given_project_python_when_listing_supported_paths_then_only_documented_roots_remain() {
let test_cases = [
SupportedPathsTestCase {
description: "documented roots sorted by parts, skipping hidden, generated and undocumented files",
files: &[
"python/orders/main.py",
"adapter.py",
"orchestration/sensors/stock.py",
"functions/python/f.py",
"models/a/_sqlbuild/hooks/python/h.py",
"helpers.py",
"models/x.py",
".hidden/x.py",
"target/x.py",
"python/__pycache__/main.py",
],
expected_supported: &[
"adapter.py",
"functions/python/f.py",
"models/a/_sqlbuild/hooks/python/h.py",
"orchestration/sensors/stock.py",
"python/orders/main.py",
],
},
SupportedPathsTestCase {
description: "a project without Python",
files: &[],
expected_supported: &[],
},
];
for test_case in test_cases {
let directory = tempfile::tempdir().expect("temporary project");
for file in test_case.files {
let path = directory.path().join(file);
std::fs::create_dir_all(path.parent().expect("parent")).expect("folders");
std::fs::write(&path, "").expect("file");
}
let supported: Vec<String> = supported_python_paths(directory.path())
.into_iter()
.map(|parts| parts.join("/"))
.collect();
assert_eq!(
supported, test_case.expected_supported,
"{}",
test_case.description
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ pub(super) struct PythonPathsTestCase {
pub(super) files: &'static [&'static str],
pub(super) expected_unsupported: &'static [&'static str],
}

/// One project layout and the Python files the Python rules read.
pub(super) struct SupportedPathsTestCase {
pub(super) description: &'static str,
pub(super) files: &'static [&'static str],
pub(super) expected_supported: &'static [&'static str],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! How a decorator expression is named.

use ruff_python_ast::Expr;

/// The decorator's last name segment, looking through a call: `udf`, `sqlbuild.udf` and `udf(...)` all name `udf`.
pub(crate) fn decorator_name(expression: &Expr) -> Option<&str> {
let target: &Expr = match expression {
Expr::Call(call) => &call.func,
_ => expression,
};
match target {
Expr::Name(name) => Some(name.id.as_str()),
Expr::Attribute(attribute) => Some(attribute.attr.as_str()),
_ => None,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod decorators;
Loading
Loading