Python API

The entry point is PolicyRuntime. It loads policies, evaluates a context against them, and looks up license data.

from ospac import PolicyRuntime, License, Policy, ComplianceResult

Those four names are ospac’s public surface. Anything under ospac.pipeline is dataset-generation machinery, not an API to build on.

PolicyRuntime

from ospac import PolicyRuntime

runtime = PolicyRuntime()                       # bundled default enterprise policy
runtime = PolicyRuntime("./policy.yaml")        # your policy
runtime = PolicyRuntime(skip_default=True)      # no policy at all

PolicyRuntime._using_default tells you whether the default policy was loaded. It is underscore-prefixed but it is what the CLI checks to emit its warning, and it is worth asserting on in automation. See Policies.

runtime = PolicyRuntime("./policy.yaml")
assert not runtime._using_default, "custom policy failed to load"

evaluate

result = runtime.evaluate(context)

context is a dict, and the keys must be the ones policy rules match on. The CLI builds this shape, and replicating it exactly is the difference between rules firing and rules being silently skipped:

context = {
    "licenses": ["GPL-3.0"],
    "licenses_found": ["GPL-3.0"],   # both keys are read; set both
    "distribution_type": "mobile",
    "distribution": "mobile",
    "context": "general",
    "linking_type": None,            # or "static_linking" / "dynamic_linking"
}

result = runtime.evaluate(context)
print(result.action)     # ActionType.DENY
print(result.message)    # 'Evaluated 1 rules'
print(result.to_dict())
{'rule_id': 'aggregate', 'action': 'deny', 'severity': 'error',
 'message': 'Evaluated 1 rules', 'requirements': [],
 'remediation': 'Replace with MIT, Apache-2.0, or BSD licensed alternative'}

Returns a PolicyResult. action is an ActionType enum, so compare against the enum or call to_dict() for the plain string:

from ospac.models.compliance import ActionType

if result.action == ActionType.DENY:
    raise SystemExit(result.to_dict()["remediation"])

A context missing a field that a rule matches on causes that rule to be skipped, not to error. If evaluation returns flag_for_review with "No policy rule matched" when you expected a denial, the rule was skipped: check the context keys first.

evaluate_licenses

result, per_license = runtime.evaluate_licenses(
    ["MIT", "MPL-2.0"],
    {"distribution_type": "commercial", "distribution": "commercial",
     "context": "general", "linking_type": None},
)

The per-license entry point, and the one the CLI uses. Each license is evaluated independently, with its own license_type resolved from the dataset, and the verdicts aggregate with most-restrictive-wins. evaluate() judges the context as a single unit, so a rule matched by one license produces a result and the no-match fail-safe never runs for the others: with evaluate(), one permissive license in the list can answer for a license that matched nothing. Use evaluate_licenses whenever the input is a list of licenses.

per_license maps each license id to its own PolicyResult, so you can report which license drove the aggregate verdict.

check_compatibility

result = runtime.check_compatibility("GPL-2.0", "Apache-2.0")
result = runtime.check_compatibility("MIT", "GPL-3.0", context="static_linking")

Returns a ComplianceResult:

>>> r = runtime.check_compatibility("GPL-2.0", "Apache-2.0")
>>> r.is_compliant
False
>>> r.violations
[{'rule_id': 'aggregate', 'message': 'Evaluated 1 rules', 'severity': 'error'}]
>>> r.required_actions
['Use GPL-3.0 (compatible with Apache-2.0) or choose different licenses']

Note that licenses_checked comes back empty here even though two licenses were compared; read the licenses from your own call arguments rather than from the result.

lookup_license_data

data = runtime.lookup_license_data("MIT")

Returns the raw record from the dataset, or None if the license is unknown. The record is wrapped in a license key, matching the file on disk:

>>> runtime.lookup_license_data("MIT").keys()
dict_keys(['license'])
>>> runtime.lookup_license_data("MIT")["license"]["type"]
'permissive'

Unwrap before use. Field meanings are in The dataset.

resolve_data_dir

>>> runtime.resolve_data_dir()
'/usr/lib/python3.12/site-packages/ospac/data'

Where ospac reads license data from. Pass a path to override.

get_obligations

PolicyRuntime.get_obligations(["MIT"]) returns {} for every license. It reads lookup_license_data()["obligations"], but obligations live at ["license"]["obligations"], so the lookup always misses. The CLI’s ospac obligations is unaffected: it uses a separate code path.

Read obligations directly until this is fixed:

record = runtime.lookup_license_data("MIT")
obligations = record["license"]["obligations"]
# ['Retain copyright notices', 'Include license text']

License

A dataclass over one license record.

from ospac import License

record = runtime.lookup_license_data("MIT")
lic = License.from_dict(record["license"])

lic.id             # 'MIT'
lic.name           # 'MIT License'
lic.type           # 'permissive'
lic.properties     # {'commercial_use': True, ...}
lic.requirements   # {'include_license': True, ...}
lic.compatibility  # per-linking-context rules

from_dict expects the unwrapped record, so pass record["license"] rather than record.

lic.is_compatible_with(other, context="static_linking")   # -> bool
lic.get_obligations()                                     # -> list[str]

License.get_obligations() recomputes obligation strings from the boolean fields rather than returning the dataset’s obligations list, so the wording differs slightly:

>>> lic.get_obligations()
['Include license text', 'Include copyright notice']
>>> record["license"]["obligations"]
['Retain copyright notices', 'Include license text']

Both describe the same duties. Use the dataset’s list when you want the same text the CLI prints.

is_compatible_with is a model-level convenience that reads the license’s own compatibility block. It does not consult your policy. Use runtime.check_compatibility() when the answer should respect policy.

ComplianceResult and PolicyResult

PolicyResult is what evaluate returns; ComplianceResult is what check_compatibility returns and the richer of the two.

result.is_compliant     # property: no violations
result.needs_review     # property: flagged rather than denied
result.violations       # list of dicts
result.warnings
result.obligations
result.required_actions
result.metadata
result.to_dict()        # JSON-serializable
from ospac import ComplianceResult
ComplianceResult.from_policy_result(policy_result)   # convert

PolicyResult.aggregate([...]) merges several results, taking the most severe action. This is what produces the rule_id: "aggregate" you see in CLI output.

A CI gate

Putting the pieces together, including the two assertions that keep the check from failing open:

import sys
from ospac import PolicyRuntime
from ospac.models.compliance import ActionType

DISTRIBUTION = "mobile"
LICENSES = ["MIT", "Apache-2.0", "GPL-3.0"]

runtime = PolicyRuntime("./compliance-policy.yaml")
if runtime._using_default:
    sys.exit("policy did not load; refusing to pass on the default policy")

result = runtime.evaluate({
    "licenses": LICENSES,
    "licenses_found": LICENSES,
    "distribution_type": DISTRIBUTION,
    "distribution": DISTRIBUTION,
    "context": "general",
    "linking_type": None,
})

detail = result.to_dict()
if result.action == ActionType.DENY:
    sys.exit(f"denied: {detail['message']}\nremediation: {detail['remediation']}")

print(f"ok: {detail['action']}")

Pair it with a case that must be denied, so the gate is proven to still bite:

gpl = runtime.evaluate({**base_context, "licenses": ["GPL-3.0"],
                        "licenses_found": ["GPL-3.0"]})
assert gpl.action == ActionType.DENY, "policy stopped denying GPL, rules are inert"