Upstream Edge Obsidian database tools and integrations
Project description
upstream-edge
A Python library for working with Obsidian databases — the SQLite files behind Upstream Edge's reserves and production forecasting application. See UpstreamEdge.com for more information.
If you've wanted to script a custom Obsidian integration with your data sources, batch-update price decks, pull production into a DataFrame, or build economic models automatically, this is the library to make it happen.
- No build tools or compilers.
pip install upstream-edgeand you're done. - Pandas if you want it. Every reader has a
_dfsibling that returns a DataFrame. Skip the install and the library never touches pandas. - Won't quietly break your database. Writes that touch more than one table run in a transaction, bad inputs raise clear errors instead of corrupting state, and broad deletes require an explicit
confirm=True. - Plays well with AI assistants. Claude, Cursor, Copilot, and the like can read the library's structured types and docstrings and write working scripts on your behalf. There's a dedicated
AGENTS.mdyou can point them to.
Installation
pip install upstream-edge # core library
pip install upstream-edge[pandas] # add DataFrame support
Requires Python 3.11 or newer.
Quickstart
from upstream_edge.obsidian_db import Database
with Database.open("wells.obsdb") as db:
for w in db.wells():
print(w.prop_id, w.lease, w.rsv_cat.value)
monthly = db.production_monthly("PROP_001")
print(f"{len(monthly)} months of production")
Writes work the same way. Wrap multi-row work in a transaction for speed.
Load production data:
from datetime import date
from upstream_edge.obsidian_db import Database, MonthlyRow
with Database.open("wells.obsdb") as db:
with db.transaction():
db.set_monthly_prod([
MonthlyRow(prop_id="PROP_001", month=date(2026, 1, 1),
oil_bbl=2150.0, gas_mscf=3420.0, water_bbl=180.0),
MonthlyRow(prop_id="PROP_001", month=date(2026, 2, 1),
oil_bbl=1980.0, gas_mscf=3180.0, water_bbl=165.0),
MonthlyRow(prop_id="PROP_001", month=date(2026, 3, 1),
oil_bbl=1840.0, gas_mscf=2950.0, water_bbl=152.0),
])
Or build out a price deck:
from datetime import date
from upstream_edge.obsidian_db import Database, PriceModelSegment
with Database.open("wells.obsdb") as db:
with db.transaction():
db.set_price_model("STRIP_2026", [
PriceModelSegment(date(2026, 1, 1), oil=72.50, gas=3.40, ngl=24.10),
PriceModelSegment(date(2027, 1, 1), oil=70.00, gas=3.20, ngl=23.00),
])
Concept tour
If you haven't worked with an Obsidian database before, this is the vocabulary:
- Databases are SQLite files, conventionally with the
.obsdbextension. - Wells are the unit of analysis. Each well has a
PropID(the library's primary identifier) and optionally anAPI10. Wells carry header info — lease, operator, dates, location, reserve category — plus links to economic models. - Production data comes in two forms: monthly (keyed to the first of the month) and daily.
- Forecasts are per-well, per-model, per-phase Arps declines. A
(prop_id, model, phase)triple can hold multiple segments, each starting on a different date. - Economic models come in five forms — price, expense, tax, differential, shrink/yield — either as shared named models or as per-well overrides under a specific scenario.
- Scenarios group a set of economic-model assignments. The
MAINscenario always exists. - Interest (WI / NRI) and capex schedules are per-well, per-model, with their own multi-segment shapes.
- Well attributes is a user-defined table — extra columns you add at runtime (text, numeric, or date).
API reference
Everything lives on the Database class.
Opening, closing, transactions
Database.open(path: str | Path) -> Database
db.close() -> None
with Database.open(path) as db: ...
db.path -> Path
db.transaction() -> ContextManager[None]
Opening gives you read/write access. Reads share the file with Obsidian normally. Write transactions raise DatabaseLockedError if Obsidian (or another process) is mid-write. Writers called outside a transaction get one implicitly; wrap multi-row work explicitly.
Readers
Every reader takes optional keyword filters; pass None to get all rows.
Wells
db.wells() -> list[Well]
db.well(prop_id: str) -> Well | None
db.well_by_api(api_10: str) -> Well | None
Production
db.production_monthly(prop_id: str | None = None) -> list[MonthlyRow]
db.production_daily(prop_id: str | None = None) -> list[DailyRow]
Forecasts
db.forecasts(prop_id: str | None = None, model: str | None = None) -> list[Forecast]
Economic models
db.price_models(name: str | None = None) -> list[PriceModelSegment]
db.expense_models(name: str | None = None) -> list[ExpenseModel]
db.tax_models(name: str | None = None) -> list[TaxModel]
db.diff_models(name: str | None = None) -> list[DiffModel]
db.shrink_yield_models(name: str | None = None) -> list[ShrinkYieldModel]
Per-well economic linkage
db.well_models(prop_id: str | None = None, scenario: str | None = None) -> list[WellModels]
db.interest(prop_id: str | None = None, model: str | None = None) -> list[Interest]
db.capex(prop_id: str | None = None, model: str | None = None) -> list[Capex]
db.abandonment(prop_id: str | None = None, model: str | None = None) -> list[Abandonment]
db.scenarios(name: str | None = None) -> list[Scenario]
Reservoir and Subsurface
db.surveys(prop_id: str | None = None) -> list[SurveyPoint]
db.reservoirs(prop_id: str | None = None) -> list[Reservoir]
db.completions(prop_id: str | None = None) -> list[Completion]
db.perfs(prop_id: str | None = None) -> list[Perfs]
Well attributes
db.list_attribute_columns() -> list[AttributeColumn]
db.well_attributes(prop_id: str) -> dict[str, str | float | date]
db.all_well_attributes() -> dict[str, dict[str, str | float | date]]
DataFrame adapter
Every reader has a _df sibling that returns a pandas DataFrame:
db.wells_df() -> pandas.DataFrame
db.production_monthly_df(prop_id=None) -> pandas.DataFrame
# ... and so on for every reader.
A few conventions across the board:
- Columns match the row dataclass fields in declaration order.
- The index is always a default
RangeIndex— time-series readers leavemonth/dateas regular columns. - Optional fields become nullable (
NaN,NaT,None). - Enums show up as their
.valuestring. - Empty results return an empty DataFrame with the right columns — never
None.
Pandas is imported lazily inside the method; if it isn't installed, you'll get MissingDependencyError.
Writers
Wells
db.add_well(prop_id: str, *, rsv_cat: RsvCat, api_10: str | None = None,
**header_fields) -> None
db.set_well_header(prop_id: str, **fields) -> None
db.copy_well(from_prop_id: str, to_prop_id: str) -> None
db.delete_well(prop_id: str, *, confirm: bool) -> None
add_well creates the well so it is immediately usable; follow with set_interest, set_abandonment, set_well_models, or set_well_attribute to customize as needed.
Production
db.set_monthly_prod(rows: list[MonthlyRow]) -> None
db.set_daily_prod(rows: list[DailyRow]) -> None
db.delete_monthly_prod(prop_id: str | None = None, *, confirm: bool = False) -> None
db.delete_daily_prod(prop_id: str | None = None, *, confirm: bool = False) -> None
Production writers take a flat list of rows — single-well or multi-well, the library groups by prop_id internally. set_monthly_prod upserts by (prop_id, month); set_daily_prod upserts by (prop_id, date).
Forecasts
db.set_forecast(prop_id: str, model: str, phase: Phase,
segments: list[ForecastSegment]) -> None
db.delete_forecast(prop_id: str | None = None, model: str | None = None,
phase: Phase | None = None, *, confirm: bool = False) -> None
Reservoir and Subsurface
db.set_surveys(prop_id: str, points: list[SurveyPointInput]) -> None
db.set_reservoir(prop_id: str, reservoir: str, *,
top_depth_ft: float,
thickness_ft: float | None = None) -> None
db.set_completion(prop_id: str, *,
frac_proppant_lb: float | None = None,
frac_fluid_bbl: float | None = None,
frac_stages: int | None = None) -> None
db.set_perfs(prop_id: str, perfs: list[PerfsInput]) -> None
db.delete_surveys(prop_id: str | None = None, *, confirm: bool = False) -> None
db.delete_reservoir_data(prop_id: str | None = None, *,
reservoir: str | None = None,
confirm: bool = False) -> None
db.delete_completion(prop_id: str, *, confirm: bool = False) -> None
db.delete_perfs(prop_id: str | None = None, *, confirm: bool = False) -> None
Whole-list writers (set_surveys, set_perfs) replace every row for the prop_id; field-level setters leave the other fields alone.
Economic models
db.set_price_model(name: str, segments: list[PriceModelSegment]) -> None
# General (named, shared) — any well can reference the same name.
db.set_expense_model(name: str, segments: list[ExpenseModelSegment]) -> None
db.set_tax_model(name: str, segments: list[TaxModelSegment]) -> None
db.set_diff_model(name: str, segments: list[DiffModelSegment]) -> None
db.set_shrink_yield_model(name: str, *,
gas_shrink_frac: float,
ngl_yield_bbl_mmscf: float) -> None
# Per-well — override the general assignment for one well in one scenario.
db.set_well_expense_model(prop_id: str, scenario: str,
segments: list[ExpenseModelSegment]) -> None
db.set_well_tax_model(prop_id: str, scenario: str,
segments: list[TaxModelSegment]) -> None
db.set_well_diff_model(prop_id: str, scenario: str,
segments: list[DiffModelSegment]) -> None
db.set_well_shrink_yield_model(prop_id: str, scenario: str, *,
gas_shrink_frac: float,
ngl_yield_bbl_mmscf: float) -> None
db.delete_price_model(name: str) -> None
db.delete_expense_model(name: str) -> None
db.delete_well_expense_model(prop_id: str, scenario: str) -> None
# Same shape for tax / diff / shrink_yield.
set_*_model writes a shared named model. set_well_*_model writes a per-well override and wires up the WellModels linkage for you.
Interest, capex, abandonment
db.set_interest(prop_ids: str | list[str], model: str,
segments: list[InterestSegment]) -> None
db.set_capex(prop_id: str, model: str, items: list[CapexItem]) -> None
db.set_abandonment(prop_id: str, model: str, cost_gross: float) -> None
db.delete_interest(prop_ids: str | list[str] | None = None,
model: str | None = None, *, confirm: bool = False) -> None
db.delete_capex(prop_id: str | None = None, model: str | None = None,
*, confirm: bool = False) -> None
set_interest writes the same schedule to every well in prop_ids (single string or list).
Scenarios and well-to-model assignment
db.create_scenario(name: str, *, copy_from: str | None = None) -> None
db.delete_scenario(name: str, *, confirm: bool) -> None
db.set_scenario(name: str, *,
forecast_model: str | None = None,
price_model: str | None = None) -> None
db.set_well_models(prop_ids: str | list[str], scenario: str, *,
exp_model: str | None = None,
capex_model: str | None = None,
diff_model: str | None = None,
tax_model: str | None = None,
shrink_yield_model: str | None = None,
interest_model: str | None = None) -> None
set_well_models is a partial update — only kwargs you pass get written. Named-entity references (exp_model, diff_model, tax_model, shrink_yield_model) are validated before the transaction opens. Label tags (capex_model, interest_model) emit a UserWarning if no matching rows exist yet — a friendly typo guard.
Well attributes
db.set_well_attribute(prop_id: str, column: str,
value: str | float | date) -> None
db.set_well_attributes_bulk(updates: list[WellAttributeUpdate]) -> None
db.add_well_attribute_column(name: str, attr_type: AttributeType,
default: str | float | date | None = None) -> None
db.rename_well_attribute_column(old: str, new: str) -> None
db.delete_well_attribute_column(name: str, *, confirm: bool) -> None
Adding a column backfills the default for every existing well.
Row dataclasses
All row types are immutable.
Read types: Well, MonthlyRow, DailyRow, Forecast, PriceModel, ExpenseModel, TaxModel, DiffModel, ShrinkYieldModel, WellModels, Interest, Capex, Abandonment, Scenario, SurveyPoint, Reservoir, Completion, Perfs, AttributeColumn.
Write-input types (body-only — the natural-key columns come from the writer's positional args): ForecastSegment, PriceModelSegment, ExpenseModelSegment, TaxModelSegment, DiffModelSegment, InterestSegment, CapexItem, WellAttributeUpdate, SurveyPointInput, PerfsInput.
MonthlyRow and DailyRow work for both reads and writes. The production writers take a flat list with each row carrying its own prop_id, so the same call works for one well or for thousands.
Enums
RsvCat = PDP | SHUT_IN | DUC | PUD | PROB | POSS |
LOC | TA | PA | SWD | UNSPECIFIED | DATA
Phase = OIL | GAS | WATER
CapexJobType = DRILLING | COMPLETIONS | FACILITIES | SURFACE_WORK |
PUMP_REPAIR | TUBING_REPAIR | ROD_REPAIR |
CASING_REPAIR | OTHER
DiffType = DOLLAR | FRACTION
AttributeType = TEXT | NUMERIC | DATE
Expense and tax model segments carry a kind plus the field that kind needs:
ExpenseModelKind = SIMPLE | AGE_BASED | DATE_BASED
TaxModelKind = SIMPLE | AGE_BASED | DATE_BASED
ExpenseModelSegment(kind=ExpenseModelKind.SIMPLE, fixed_monthly=2500.0, ...)
ExpenseModelSegment(kind=ExpenseModelKind.AGE_BASED, age_months=18,
fixed_monthly=2500.0, ...)
ExpenseModelSegment(kind=ExpenseModelKind.DATE_BASED,
effective_date=date(2026, 1, 1),
fixed_monthly=2500.0, ...)
SIMPLE rejects both age_months and effective_date; AGE_BASED requires age_months; DATE_BASED requires effective_date. Same shape on TaxModelSegment.
Exceptions
All inherit from ObsidianDbError.
| Exception | When |
|---|---|
DatabaseLockedError |
Another process holds the SQLite write lock during a write transaction. |
DataIntegrityError |
The database contains data the library can't interpret (e.g. missing column). |
WellNotFoundError |
A PropID you referenced doesn't exist. Carries .prop_id. |
ModelNotFoundError |
A named model you referenced doesn't exist. Carries .model_kind and .name. |
DuplicateError |
An add_* was called for a key that already exists. |
ValidationError |
Invalid input — bad value, unknown column, mutually-exclusive kwargs. |
MissingDependencyError |
Pandas isn't installed and you called a _df method. |
Error messages name the method, the offending input, and the valid alternatives where the API can determine them.
Values and conventions
- Dates use
datetime.date. Monthly fields are keyed to the first of the month. - Percentages are decimals —
0.875for 87.5%. Anything outside[0, 1]raisesValidationError. - Volumes: oil in barrels (
bbl), gas in MSCF, water in barrels. Daily rates usebopd/mcfd/bwpd. - Identifiers:
PropIDis a plain string;API10is exactly ten digits. - Enums round-trip as their
.valuestring. - Empty inputs to whole-replace writers raise
ValidationErrorand point at the correspondingdelete_*method. Empty inputs to per-key upserts are a silent no-op. set_*is idempotent. Safe to retry after a hiccup.- Broad deletes require
confirm=True. The library would rather make you type one extra word than vaporize a quarter's work by accident.
Logging
The library logs to logging.getLogger("upstream_edge.obsidian_db") with a NullHandler attached at import. Configure handlers in your application; the library never calls print.
Thread safety
Database instances are not thread-safe. Open one per thread.
Cookbook
Worked recipes for the common things people do. Each block stands alone — copy, paste, edit the names, run.
List every well in a database
from upstream_edge.obsidian_db import Database
with Database.open("wells.obsdb") as db:
for w in db.wells():
print(w.prop_id, w.lease, w.rsv_cat.value)
Multiple writes in one transaction
from upstream_edge.obsidian_db import Database, RsvCat
with Database.open("wells.obsdb") as db:
with db.transaction():
db.add_well("PROP_999", rsv_cat=RsvCat.PUD, lease="NEW LEASE 1H")
db.set_well_header("PROP_999", operator="Mitchell", state="TX", county="Midland")
Filter wells and pull first-prod dates
from upstream_edge.obsidian_db import Database, RsvCat
with Database.open("wells.obsdb") as db:
targets = [w for w in db.wells()
if w.rsv_cat == RsvCat.PUD and w.operator == "Mitchell"]
for w in targets:
print(w.prop_id, w.first_prod)
Export monthly production to a DataFrame
with Database.open("wells.obsdb") as db:
df = db.production_monthly_df("PROP_001")
df.to_csv("prop_001_monthly.csv", index=False)
Cumulative oil through an as-of date
from datetime import date
with Database.open("wells.obsdb") as db:
cutoff = date(2026, 1, 1)
for prop_id in ("PROP_001", "PROP_002"):
rows = db.production_monthly(prop_id)
cum = sum(r.oil_bbl or 0.0 for r in rows if r.month < cutoff)
print(prop_id, f"{cum:,.0f} bbl")
Three-segment price model
from datetime import date
from upstream_edge.obsidian_db import Database, PriceModelSegment
with Database.open("wells.obsdb") as db:
db.set_price_model("STRIP_2026", [
PriceModelSegment(date(2026, 1, 1), oil=72.50, gas=3.40, ngl=24.10),
PriceModelSegment(date(2027, 1, 1), oil=70.00, gas=3.20, ngl=23.00),
PriceModelSegment(date(2028, 1, 1), oil=68.00, gas=3.10, ngl=22.00),
])
Simple expense model assigned to a scenario
from upstream_edge.obsidian_db import (
Database, ExpenseModelSegment, ExpenseModelKind,
)
with Database.open("wells.obsdb") as db:
with db.transaction():
db.set_expense_model(
"STD_OPEX",
segments=[ExpenseModelSegment(
kind=ExpenseModelKind.SIMPLE,
fixed_monthly=2500.0,
variable_oil=4.50, variable_gas=0.30, variable_water=1.10,
)],
)
db.set_well_models(
["PROP_001", "PROP_002"], scenario="MAIN",
exp_model="STD_OPEX",
)
Age-based tax model
from upstream_edge.obsidian_db import (
Database, TaxModelSegment, TaxModelKind,
)
with Database.open("wells.obsdb") as db:
db.set_tax_model(
"TX_PERMIAN",
segments=[
TaxModelSegment(
kind=TaxModelKind.AGE_BASED, age_months=18,
sev_tax_oil=0.046, sev_tax_gas=0.075,
sev_tax_ngl=0.046, ad_valorum_tax=0.025,
),
TaxModelSegment(
kind=TaxModelKind.SIMPLE,
sev_tax_oil=0.046, sev_tax_gas=0.075,
sev_tax_ngl=0.046, ad_valorum_tax=0.025,
),
],
)
Date-based diff model
from datetime import date
from upstream_edge.obsidian_db import (
Database, DiffModelSegment, DiffType,
)
with Database.open("wells.obsdb") as db:
db.set_diff_model(
"MIDLAND_DIFF",
segments=[DiffModelSegment(
start_date=date(2026, 1, 1),
oil_method=DiffType.DOLLAR, oil_diff=-2.50,
gas_method=DiffType.FRACTION, gas_diff=0.92,
ngl_method=DiffType.FRACTION, ngl_diff=0.35,
)],
)
Per-well expense model (one well, one scenario)
with Database.open("wells.obsdb") as db:
db.set_well_expense_model(
"PROP_007", scenario="MAIN",
segments=[ExpenseModelSegment(
kind=ExpenseModelKind.SIMPLE,
fixed_monthly=4800.0,
variable_oil=6.20, variable_gas=0.45, variable_water=1.80,
)],
)
Single-segment Arps decline forecast
from datetime import date
from upstream_edge.obsidian_db import Database, ForecastSegment, Phase
with Database.open("wells.obsdb") as db:
db.set_forecast("PROP_001", model="BASE", phase=Phase.OIL,
segments=[ForecastSegment(
start=date(2026, 6, 1),
rate_init=450.0,
decline_init=0.65,
b_factor=1.1,
decline_min=0.06,
)])
Multi-segment forecast (flush, decline, terminal)
with Database.open("wells.obsdb") as db:
db.set_forecast("PROP_001", model="BASE", phase=Phase.OIL, segments=[
ForecastSegment(start=date(2026, 6, 1), rate_init=620.0, decline_init=0.95, b_factor=1.4, decline_min=0.06),
ForecastSegment(start=date(2026, 12, 1), rate_init=410.0, decline_init=0.65, b_factor=1.1, decline_min=0.06),
ForecastSegment(start=date(2030, 1, 1), rate_init=120.0, decline_init=0.06, b_factor=0.0, decline_min=0.06),
])
Bulk-import monthly production for many wells
import csv
from datetime import date
from upstream_edge.obsidian_db import Database, MonthlyRow
with Database.open("wells.obsdb") as db, db.transaction():
with open("production.csv") as f:
rows = [MonthlyRow(
prop_id=r["prop_id"],
month=date.fromisoformat(r["month"]),
oil_bbl=float(r["oil_bbl"]),
gas_mscf=float(r["gas_mscf"]),
water_bbl=float(r["water_bbl"]),
) for r in csv.DictReader(f)]
db.set_monthly_prod(rows)
Replace a well's directional survey
from upstream_edge.obsidian_db import SurveyPointInput
points = [SurveyPointInput(
point_md=md, point_tvd=tvd,
azimuth_angle=az, inclination_angle=incl,
deviation_ns=ns, deviation_ew=ew,
) for md, tvd, az, incl, ns, ew in load_survey_csv("survey.csv")]
with Database.open("wells.obsdb") as db:
db.set_surveys("PROP_001", points)
Add a custom date attribute and backfill from CSV
import csv
from datetime import date
from upstream_edge.obsidian_db import (
Database, AttributeType, WellAttributeUpdate,
)
with Database.open("wells.obsdb") as db, db.transaction():
db.add_well_attribute_column("LeaseExpiry", AttributeType.DATE)
updates = []
with open("lease_expiry.csv") as f:
for r in csv.DictReader(f):
updates.append(WellAttributeUpdate(
prop_id=r["prop_id"],
column="LeaseExpiry",
value=date.fromisoformat(r["expiry"]),
))
db.set_well_attributes_bulk(updates)
Copy a scenario and swap its diff model
with Database.open("wells.obsdb") as db, db.transaction():
db.create_scenario("MIDLAND_DIFF_TEST", copy_from="MAIN")
prop_ids = [wm.prop_id for wm in db.well_models(scenario="MIDLAND_DIFF_TEST")]
db.set_well_models(prop_ids, scenario="MIDLAND_DIFF_TEST",
diff_model="MIDLAND_DIFF")
Bulk-update multi-segment interest for 200 wells
from datetime import date
from upstream_edge.obsidian_db import Database, InterestSegment
prop_ids = [f"PROP_{i:03d}" for i in range(1, 201)]
segments = [
InterestSegment(start=date(2026, 1, 1), wi_pct=1.00, nri_pct=0.75),
InterestSegment(start=date(2031, 1, 1), wi_pct=1.00, nri_pct=0.80),
]
with Database.open("wells.obsdb") as db, db.transaction():
db.set_interest(prop_ids, model="MAIN", segments=segments)
Working with AI assistants
AI coding assistants — Claude, Cursor, Copilot, and the rest — drive this library well. Point one at your repository and ask it to "pull last quarter's production for every Permian well into a CSV" or "rebuild the STRIP_2026 price deck from this spreadsheet" and you should get something runnable on the first pass.
A few things make that work:
AGENTS.mdships at the repository root. It's a dense, agent-targeted bootstrap doc — domain glossary, do's and don'ts, common pitfalls, a startup sequence, and an FAQ. One read and the agent is oriented; you don't have to write the prompt yourself.- Methods have typed signatures and source docstrings. Agents can inspect the public API directly instead of guessing table names or row shapes.
- Errors point at the fix. Misspell a field and the exception names the method, the bad value, and the valid alternatives when available.
- Broad destructive operations need
confirm=True. An agent can't quietly wipe a table; it has to name the flag.
A reasonable workflow for an agent: read AGENTS.md, open the database, then do its work inside a transaction().
License
Apache License 2.0. See LICENSE.
Contributing
Issues and pull requests are welcome.
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 upstream_edge-1.0.1.tar.gz.
File metadata
- Download URL: upstream_edge-1.0.1.tar.gz
- Upload date:
- Size: 46.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40ab6528f87d8cd2f1ea944b072572d831a43b1c320c6a6ff0fae8bdf2b8cd70
|
|
| MD5 |
130da9e1393f3f5cc472aa4fbf418d35
|
|
| BLAKE2b-256 |
ca1ff99fdd598aedbd7063ea53f785ae1cadca59fbda570ec7ec4e66d33f0aaa
|
Provenance
The following attestation bundles were made for upstream_edge-1.0.1.tar.gz:
Publisher:
publish.yml on UpstreamEdge/upstream_edge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
upstream_edge-1.0.1.tar.gz -
Subject digest:
40ab6528f87d8cd2f1ea944b072572d831a43b1c320c6a6ff0fae8bdf2b8cd70 - Sigstore transparency entry: 1632034629
- Sigstore integration time:
-
Permalink:
UpstreamEdge/upstream_edge@5f0863683a57127deec9fffc651139d64eb5cbd3 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/UpstreamEdge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f0863683a57127deec9fffc651139d64eb5cbd3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file upstream_edge-1.0.1-py3-none-any.whl.
File metadata
- Download URL: upstream_edge-1.0.1-py3-none-any.whl
- Upload date:
- Size: 40.4 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 |
1599878f070ba6b5452ac30bdcc1d197d0e2b9d73d65e3791c81f0da423407d9
|
|
| MD5 |
bd91cb54f0d1a778faaf255b5864c349
|
|
| BLAKE2b-256 |
9a2614a6780c6f2aa104a5026357f1611be938321e431a479032fab4a19e80ee
|
Provenance
The following attestation bundles were made for upstream_edge-1.0.1-py3-none-any.whl:
Publisher:
publish.yml on UpstreamEdge/upstream_edge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
upstream_edge-1.0.1-py3-none-any.whl -
Subject digest:
1599878f070ba6b5452ac30bdcc1d197d0e2b9d73d65e3791c81f0da423407d9 - Sigstore transparency entry: 1632034672
- Sigstore integration time:
-
Permalink:
UpstreamEdge/upstream_edge@5f0863683a57127deec9fffc651139d64eb5cbd3 -
Branch / Tag:
refs/tags/v1.0.1 - Owner: https://github.com/UpstreamEdge
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5f0863683a57127deec9fffc651139d64eb5cbd3 -
Trigger Event:
release
-
Statement type: