- bcda/client.py: output_dir default → conf.path("storage.bcda")
- bcda/express/flatten.py: store_path default → conf.path("storage.bcda")
- notebooks: eliminate all /home/kert/ absolute paths from
bib_explorer.py and zotero_tutorial.py
- generate_quality_measure_docs.py: complete truncated f-string at
line 561 (file was committed incomplete), add return statement
- test_client.py: update default assertion to match conf-resolved path
744 lines
21 KiB
Python
744 lines
21 KiB
Python
import marimo
|
|
|
|
__generated_with = "0.19.9"
|
|
app = marimo.App(width="medium")
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _():
|
|
import marimo as mo
|
|
|
|
mo.md("""
|
|
# Bibliography Explorer
|
|
|
|
Browse, search, and cite from the **bib** reference library — a denormalized SQLite store
|
|
that replaces Zotero's 61-table schema with 8 clean tables.
|
|
|
|
This notebook provides:
|
|
|
|
1. **Migration** — one-click import from the Zotero container database
|
|
2. **Library overview** — stats, collections, tags
|
|
3. **Browse & search** — filter by type, tag, collection, or free text
|
|
4. **Item inspector** — full metadata for any item
|
|
5. **Citations** — APA and Bluebook formatted references
|
|
6. **DataFrame export** — polars DataFrame for downstream analysis
|
|
""")
|
|
return (mo,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
from pathlib import Path
|
|
|
|
from bib.client import COLLECTIONS
|
|
from bib.format import format_bibliography, format_citation
|
|
from bib.item import Download, Item, Manual, Regulation, Rule, Source
|
|
from bib.store import Store
|
|
from bib.tag import Tag, filter_tags
|
|
|
|
from conf import path as _conf_path
|
|
|
|
BIB_DB = _conf_path("db.bib")
|
|
ZOTERO_DB = _conf_path("db.zotero")
|
|
|
|
store = Store(str(BIB_DB))
|
|
|
|
mo.md(f"""
|
|
## 1. Connect
|
|
|
|
| Setting | Value |
|
|
|---------|-------|
|
|
| Database | `{BIB_DB}` |
|
|
| Exists | **{BIB_DB.exists()}** |
|
|
| Zotero source | `{ZOTERO_DB}` |
|
|
| Zotero available | **{ZOTERO_DB.exists()}** |
|
|
""")
|
|
return (
|
|
COLLECTIONS,
|
|
Download,
|
|
Manual,
|
|
Regulation,
|
|
Rule,
|
|
Source,
|
|
Tag,
|
|
ZOTERO_DB,
|
|
format_citation,
|
|
store,
|
|
)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(ZOTERO_DB, mo, store):
|
|
_item_count = store._con().execute("SELECT COUNT(*) FROM items").fetchone()[0]
|
|
|
|
if _item_count > 0:
|
|
_msg = f"""
|
|
## 2. Migration
|
|
|
|
Database already has **{_item_count}** items — migration not needed.
|
|
|
|
To re-migrate, delete `data/bib.sqlite` and restart.
|
|
"""
|
|
elif not ZOTERO_DB.exists():
|
|
_msg = """
|
|
## 2. Migration
|
|
|
|
No Zotero database found at `{ZOTERO_DB}`.
|
|
Start the Zotero container first, or add items manually below.
|
|
"""
|
|
else:
|
|
_msg = """
|
|
## 2. Migration
|
|
|
|
Database is empty and Zotero source is available. Click **Migrate** to import.
|
|
"""
|
|
|
|
mo.md(_msg)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(ZOTERO_DB, mo, store):
|
|
_count = store._con().execute("SELECT COUNT(*) FROM items").fetchone()[0]
|
|
_show_migrate = _count == 0 and ZOTERO_DB.exists()
|
|
|
|
migrate_btn = mo.ui.run_button(label="Migrate from Zotero") if _show_migrate else None
|
|
migrate_btn
|
|
return (migrate_btn,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(ZOTERO_DB, migrate_btn, mo, store):
|
|
if migrate_btn is None or not migrate_btn.value:
|
|
mo.stop(True)
|
|
|
|
# Inline migration — reads Zotero EAV, writes to the bib store
|
|
import sqlite3
|
|
|
|
zcon = sqlite3.connect(f"file:{ZOTERO_DB}?mode=ro", uri=True)
|
|
zcon.row_factory = sqlite3.Row
|
|
|
|
# Migrate collections
|
|
_zot_cols = zcon.execute(
|
|
"""SELECT collectionID, collectionName, key, parentCollectionID
|
|
FROM collections WHERE libraryID = 1
|
|
ORDER BY parentCollectionID NULLS FIRST"""
|
|
).fetchall()
|
|
_col_id_to_key = {r["collectionID"]: r["key"] for r in _zot_cols}
|
|
bcon = store._con()
|
|
for _zc in _zot_cols:
|
|
_pid = None
|
|
if _zc["parentCollectionID"]:
|
|
_pk = _col_id_to_key.get(_zc["parentCollectionID"])
|
|
if _pk:
|
|
_pr = bcon.execute("SELECT id FROM collections WHERE key = ?", (_pk,)).fetchone()
|
|
if _pr:
|
|
_pid = _pr["id"]
|
|
bcon.execute(
|
|
"INSERT OR IGNORE INTO collections (key, name, parent_id) VALUES (?, ?, ?)",
|
|
(_zc["key"], _zc["collectionName"], _pid),
|
|
)
|
|
bcon.commit()
|
|
|
|
# Migrate content items
|
|
_content = zcon.execute(
|
|
"""SELECT i.itemID, i.key, it.typeName
|
|
FROM items i JOIN itemTypes it ON i.itemTypeID = it.itemTypeID
|
|
WHERE i.libraryID = 1
|
|
AND i.itemID NOT IN (SELECT itemID FROM deletedItems)
|
|
AND it.typeName NOT IN ('attachment', 'note')
|
|
ORDER BY i.itemID"""
|
|
).fetchall()
|
|
|
|
_migrated = 0
|
|
for _zi in _content:
|
|
# Read fields via EAV join
|
|
_fields = {
|
|
r["fieldName"]: r["value"]
|
|
for r in zcon.execute(
|
|
"""SELECT f.fieldName, idv.value FROM itemData id
|
|
JOIN fields f ON id.fieldID = f.fieldID
|
|
JOIN itemDataValues idv ON id.valueID = idv.valueID
|
|
WHERE id.itemID = ?""",
|
|
(_zi["itemID"],),
|
|
).fetchall()
|
|
}
|
|
|
|
# Classify
|
|
_zt = _zi["typeName"]
|
|
if _zt == "statute":
|
|
_it = "regulation" if "C.F.R." in _fields.get("code", "") else "rule"
|
|
elif _zt == "report":
|
|
_it = "manual"
|
|
elif _zt == "webpage":
|
|
_it = "download"
|
|
else:
|
|
_it = "source"
|
|
|
|
# Title
|
|
_title = _fields.get("nameOfAct", "") if _it in ("rule", "regulation") else _fields.get("title", "")
|
|
_date = _fields.get("dateEnacted", "") if _it in ("rule", "regulation") else _fields.get("date", "")
|
|
|
|
bcon.execute(
|
|
"""INSERT OR IGNORE INTO items
|
|
(key, item_type, title, url, date_published, access_date, abstract, institution, extra, extra_json)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(
|
|
_zi["key"], _it, _title,
|
|
_fields.get("url", ""), _date,
|
|
_fields.get("accessDate", ""), _fields.get("abstractNote", ""),
|
|
_fields.get("institution", "") or _fields.get("publisher", ""),
|
|
_fields.get("extra", ""), "{}",
|
|
),
|
|
)
|
|
|
|
_bib_row = bcon.execute("SELECT id FROM items WHERE key = ?", (_zi["key"],)).fetchone()
|
|
if _bib_row is None:
|
|
continue
|
|
_bid = _bib_row["id"]
|
|
_migrated += 1
|
|
|
|
# Tags
|
|
for _tr in zcon.execute(
|
|
"SELECT t.name FROM itemTags it JOIN tags t ON it.tagID = t.tagID WHERE it.itemID = ?",
|
|
(_zi["itemID"],),
|
|
).fetchall():
|
|
_tid = store._ensure_tag(_tr["name"])
|
|
bcon.execute("INSERT OR IGNORE INTO item_tags (item_id, tag_id) VALUES (?, ?)", (_bid, _tid))
|
|
|
|
# Collection membership
|
|
for _cr in zcon.execute(
|
|
"SELECT c.key FROM collectionItems ci JOIN collections c ON ci.collectionID = c.collectionID WHERE ci.itemID = ?",
|
|
(_zi["itemID"],),
|
|
).fetchall():
|
|
_cid_row = bcon.execute("SELECT id FROM collections WHERE key = ?", (_cr["key"],)).fetchone()
|
|
if _cid_row:
|
|
bcon.execute(
|
|
"INSERT OR IGNORE INTO collection_items (collection_id, item_id) VALUES (?, ?)",
|
|
(_cid_row["id"], _bid),
|
|
)
|
|
|
|
bcon.commit()
|
|
zcon.close()
|
|
|
|
mo.md(f"Migrated **{_migrated}** items from Zotero.")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(COLLECTIONS, mo, store):
|
|
_cols = store.ensure_collections(COLLECTIONS)
|
|
mo.md(f"Ensured **{len(_cols)}** collections.")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 3. Library Overview
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
_con = store._con()
|
|
|
|
_stats = {}
|
|
_stats["Items"] = _con.execute("SELECT COUNT(*) FROM items").fetchone()[0]
|
|
_stats["Collections"] = _con.execute("SELECT COUNT(*) FROM collections").fetchone()[0]
|
|
_stats["Tags"] = _con.execute("SELECT COUNT(*) FROM tags").fetchone()[0]
|
|
_stats["Creators"] = _con.execute("SELECT COUNT(*) FROM creators").fetchone()[0]
|
|
_stats["Attachments"] = _con.execute("SELECT COUNT(*) FROM attachments").fetchone()[0]
|
|
_stats["Notes"] = _con.execute("SELECT COUNT(*) FROM notes").fetchone()[0]
|
|
|
|
_stat_table = "\n".join(f"| {k} | {v:,} |" for k, v in _stats.items())
|
|
mo.md(f"""
|
|
| Metric | Count |
|
|
|--------|-------|
|
|
{_stat_table}
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
### Item Types
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
import polars as pl
|
|
|
|
_types_rows = store._con().execute(
|
|
"""SELECT item_type, COUNT(*) as count
|
|
FROM items GROUP BY item_type ORDER BY count DESC"""
|
|
).fetchall()
|
|
_types_df = pl.DataFrame([dict(r) for r in _types_rows]) if _types_rows else pl.DataFrame()
|
|
|
|
mo.ui.table(_types_df, label="Item Types") if _types_df.height > 0 else mo.md("No items yet.")
|
|
return (pl,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
### Collections
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
_collections = store.list_collections()
|
|
|
|
# Build a tree from the flat list
|
|
_by_key = {c["key"]: c for c in _collections}
|
|
_children: dict[str, list] = {}
|
|
_roots = []
|
|
for _c in _collections:
|
|
pk = _c["parent_key"]
|
|
if pk:
|
|
_children.setdefault(pk, []).append(_c)
|
|
else:
|
|
_roots.append(_c)
|
|
|
|
# Build tree iteratively (recursive local fns break in marimo cells)
|
|
_tree_lines = []
|
|
_stack = [(n, 0) for n in sorted(_roots, key=lambda x: x["name"], reverse=True)]
|
|
while _stack:
|
|
_node, _depth = _stack.pop()
|
|
_tree_lines.append(f"{' ' * _depth}- **{_node['name']}** ({_node['item_count']} items)")
|
|
for _ch in sorted(_children.get(_node["key"], []), key=lambda x: x["name"], reverse=True):
|
|
_stack.append((_ch, _depth + 1))
|
|
mo.md("\n".join(_tree_lines) if _tree_lines else "No collections. Ensure collections above.")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
### Tags
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, pl, store):
|
|
_tag_list = store.list_tags()
|
|
_tag_df = pl.DataFrame(_tag_list) if _tag_list else pl.DataFrame()
|
|
|
|
mo.ui.table(_tag_df, label="Tags") if _tag_df.height > 0 else mo.md("No tags yet.")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 4. Browse & Search
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
_type_opts = [""] + sorted({
|
|
r[0] for r in store._con().execute(
|
|
"SELECT DISTINCT item_type FROM items"
|
|
).fetchall()
|
|
})
|
|
_tag_opts = [""] + [
|
|
t["name"] for t in store.list_tags()
|
|
]
|
|
_col_opts = [""] + [
|
|
f"{c['name']}|{c['key']}" for c in store.list_collections()
|
|
]
|
|
|
|
search_input = mo.ui.text(placeholder="Search titles and abstracts...", label="Search", full_width=True)
|
|
type_filter = mo.ui.dropdown(options=_type_opts, value="", label="Type")
|
|
tag_filter = mo.ui.dropdown(options=_tag_opts, value="", label="Tag")
|
|
col_filter = mo.ui.dropdown(options=_col_opts, value="", label="Collection")
|
|
|
|
mo.hstack([search_input], widths=[1])
|
|
mo.hstack([type_filter, tag_filter, col_filter], widths=[1, 1, 1])
|
|
return col_filter, search_input, tag_filter, type_filter
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(col_filter, mo, pl, search_input, store, tag_filter, type_filter):
|
|
_filters = {}
|
|
if search_input.value:
|
|
_filters["query"] = search_input.value.strip()
|
|
if type_filter.value:
|
|
_filters["item_type"] = type_filter.value
|
|
if tag_filter.value:
|
|
_filters["tag"] = tag_filter.value
|
|
if col_filter.value and "|" in col_filter.value:
|
|
_filters["collection"] = col_filter.value.split("|")[1]
|
|
|
|
_items = store.list_items(**_filters)
|
|
|
|
_rows = [
|
|
{
|
|
"key": it.key,
|
|
"type": it.item_type,
|
|
"title": it.title[:100],
|
|
"date": it.date_published,
|
|
"tags": "; ".join(it.tags[:4]),
|
|
"url": it.url[:60] if it.url else "",
|
|
}
|
|
for it in _items
|
|
]
|
|
_browse_df = pl.DataFrame(_rows) if _rows else pl.DataFrame()
|
|
|
|
_label = f"Items ({len(_items)} total)"
|
|
if _browse_df.height > 0:
|
|
items_table = mo.ui.table(_browse_df, selection="single", label=_label)
|
|
else:
|
|
items_table = None
|
|
mo.output.replace(mo.md("No items match the current filters."))
|
|
return (items_table,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 5. Item Inspector
|
|
|
|
Select a row above, or enter a key below.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(items_table, mo):
|
|
_default_key = ""
|
|
if items_table is not None:
|
|
_val = items_table.value
|
|
import polars as _pl
|
|
if isinstance(_val, _pl.DataFrame) and _val.height > 0:
|
|
_default_key = _val["key"][0]
|
|
elif isinstance(_val, list) and len(_val) > 0:
|
|
_default_key = _val[0].get("key", "") if isinstance(_val[0], dict) else ""
|
|
|
|
key_input = mo.ui.text(value=_default_key, label="Item Key", full_width=False)
|
|
key_input
|
|
return (key_input,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(format_citation, key_input, mo, store):
|
|
_key = key_input.value.strip() if key_input.value else ""
|
|
|
|
if not _key:
|
|
mo.md("Enter an item key above.")
|
|
mo.stop(True)
|
|
|
|
try:
|
|
_item = store.get(_key)
|
|
except KeyError:
|
|
mo.md(f"Item **{_key}** not found.")
|
|
mo.stop(True)
|
|
|
|
_sections = [f"### {_item.title}\n"]
|
|
|
|
# Metadata table
|
|
_sections.append("| Field | Value |")
|
|
_sections.append("|-------|-------|")
|
|
_sections.append(f"| Key | `{_item.key}` |")
|
|
_sections.append(f"| Type | {_item.item_type} |")
|
|
if _item.date_published:
|
|
_sections.append(f"| Published | {_item.date_published} |")
|
|
if _item.institution:
|
|
_sections.append(f"| Institution | {_item.institution} |")
|
|
if _item.url:
|
|
_sections.append(f"| URL | {_item.url[:80]} |")
|
|
if _item.access_date:
|
|
_sections.append(f"| Accessed | {_item.access_date} |")
|
|
|
|
# Type-specific fields from extra_json
|
|
import json as _json
|
|
|
|
_row = _item.to_row()
|
|
_ej = _json.loads(_row.get("extra_json", "{}"))
|
|
for _field, _val in _ej.items():
|
|
if _val:
|
|
_sections.append(f"| {_field} | {str(_val)[:80]} |")
|
|
|
|
# Tags
|
|
if _item.tags:
|
|
_sections.append(f"\n**Tags**: {', '.join(f'`{t}`' for t in _item.tags)}")
|
|
|
|
# Abstract
|
|
if _item.abstract:
|
|
_sections.append(f"\n**Abstract**: {_item.abstract[:300]}{'...' if len(_item.abstract) > 300 else ''}")
|
|
|
|
# Citation preview
|
|
_cite_apa = format_citation(_item, style="apa")
|
|
_sections.append(f"\n**APA Citation**:\n> {_cite_apa}")
|
|
|
|
if _item.item_type == "regulation":
|
|
_cite_bb = format_citation(_item, style="bluebook")
|
|
_sections.append(f"\n**Bluebook Citation**:\n> {_cite_bb}")
|
|
|
|
# Attachments
|
|
_atts = store._con().execute(
|
|
"""SELECT key, filename, content_type FROM attachments
|
|
WHERE item_id = (SELECT id FROM items WHERE key = ?)""",
|
|
(_key,),
|
|
).fetchall()
|
|
if _atts:
|
|
_sections.append("\n**Attachments**")
|
|
for _a in _atts:
|
|
_sections.append(f"- `{_a['filename']}` ({_a['content_type'] or 'unknown'})")
|
|
|
|
# Notes
|
|
_notes = store._con().execute(
|
|
"""SELECT title, content FROM notes
|
|
WHERE item_id = (SELECT id FROM items WHERE key = ?)""",
|
|
(_key,),
|
|
).fetchall()
|
|
if _notes:
|
|
_sections.append("\n**Notes**")
|
|
for _n in _notes:
|
|
_title = _n["title"] or "(untitled)"
|
|
_content = _n["content"][:200] if _n["content"] else ""
|
|
_sections.append(f"- **{_title}**: {_content}")
|
|
|
|
mo.md("\n".join(_sections))
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 6. Create an Item
|
|
|
|
Add a new citation manually using one of the five item types.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
new_type = mo.ui.dropdown(
|
|
options=["rule", "manual", "regulation", "download", "source"],
|
|
value="rule",
|
|
label="Item Type",
|
|
)
|
|
new_title = mo.ui.text(placeholder="Title", label="Title", full_width=True)
|
|
new_url = mo.ui.text(placeholder="URL", label="URL", full_width=True)
|
|
new_tags_input = mo.ui.text(placeholder="module:pfs, year:2026", label="Tags (comma-separated)", full_width=True)
|
|
create_btn = mo.ui.run_button(label="Create Item")
|
|
|
|
mo.vstack([
|
|
mo.hstack([new_type, new_title], widths=[1, 3]),
|
|
new_url,
|
|
mo.hstack([new_tags_input, create_btn], widths=[3, 1]),
|
|
])
|
|
return create_btn, new_tags_input, new_title, new_type, new_url
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(
|
|
Download,
|
|
Manual,
|
|
Regulation,
|
|
Rule,
|
|
Source,
|
|
Tag,
|
|
create_btn,
|
|
mo,
|
|
new_tags_input,
|
|
new_title,
|
|
new_type,
|
|
new_url,
|
|
store,
|
|
):
|
|
if not create_btn.value:
|
|
mo.stop(True)
|
|
|
|
_title = new_title.value.strip() if new_title.value else ""
|
|
if not _title:
|
|
mo.md("Enter a title.")
|
|
mo.stop(True)
|
|
|
|
_type_map = {
|
|
"rule": Rule,
|
|
"manual": Manual,
|
|
"regulation": Regulation,
|
|
"download": Download,
|
|
"source": Source,
|
|
}
|
|
_cls = _type_map[new_type.value]
|
|
_item = _cls(title=_title, url=new_url.value.strip() if new_url.value else "")
|
|
|
|
# Parse tags
|
|
_tags = []
|
|
if new_tags_input.value:
|
|
for _t in new_tags_input.value.split(","):
|
|
_t = _t.strip()
|
|
if ":" in _t:
|
|
_tags.append(Tag.from_label(_t))
|
|
|
|
_key = store.upsert(_item, tags=_tags)
|
|
mo.md(f"Created item `{_key}`: **{_title}**")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 7. Use a Translator
|
|
|
|
Fetch and parse a CMS source page directly into a citation.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
translator_type = mo.ui.dropdown(
|
|
options=["cms_manual", "cms_website", "ecfr"],
|
|
value="cms_manual",
|
|
label="Translator",
|
|
)
|
|
translator_url = mo.ui.text(
|
|
placeholder="https://www.cms.gov/...",
|
|
label="URL",
|
|
full_width=True,
|
|
)
|
|
translate_btn = mo.ui.run_button(label="Translate & Save")
|
|
|
|
mo.hstack([translator_type, translator_url, translate_btn], widths=[1, 3, 1])
|
|
return translate_btn, translator_type, translator_url
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(
|
|
format_citation,
|
|
mo,
|
|
store,
|
|
translate_btn,
|
|
translator_type,
|
|
translator_url,
|
|
):
|
|
if not translate_btn.value:
|
|
mo.stop(True)
|
|
|
|
_url = translator_url.value.strip() if translator_url.value else ""
|
|
if not _url:
|
|
mo.md("Enter a URL to translate.")
|
|
mo.stop(True)
|
|
|
|
from bib import translate
|
|
|
|
_translators = {
|
|
"cms_manual": translate.cms_manual,
|
|
"cms_website": translate.cms_website,
|
|
"ecfr": translate.ecfr,
|
|
}
|
|
|
|
try:
|
|
_fn = _translators[translator_type.value]
|
|
_item = _fn(_url)
|
|
_key = store.upsert(_item)
|
|
_cite = format_citation(_item, style="apa")
|
|
_msg = f"""
|
|
Saved as `{_key}`: **{_item.title}**
|
|
|
|
Tags: {', '.join(f'`{t}`' for t in _item.tags)}
|
|
|
|
> {_cite}
|
|
"""
|
|
except Exception as e:
|
|
_msg = f"Translation failed: `{e}`"
|
|
|
|
mo.md(_msg)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 8. Bibliography Generator
|
|
|
|
Select items by tag or type to generate a formatted reference list.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
_bib_tag_opts = [""] + [t["name"] for t in store.list_tags()]
|
|
_bib_type_opts = [""] + sorted({
|
|
r[0] for r in store._con().execute(
|
|
"SELECT DISTINCT item_type FROM items"
|
|
).fetchall()
|
|
})
|
|
|
|
bib_tag = mo.ui.dropdown(options=_bib_tag_opts, value="", label="Filter by tag")
|
|
bib_type = mo.ui.dropdown(options=_bib_type_opts, value="", label="Filter by type")
|
|
bib_style = mo.ui.dropdown(options=["apa", "bluebook"], value="apa", label="Style")
|
|
|
|
mo.hstack([bib_tag, bib_type, bib_style], widths=[1, 1, 1])
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## 9. Export to DataFrame
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo, store):
|
|
try:
|
|
library_df = store.to_dataframe()
|
|
_msg = f"Exported **{library_df.height}** items to `library_df`"
|
|
except Exception:
|
|
library_df = None
|
|
_msg = "No items to export."
|
|
|
|
mo.md(_msg)
|
|
return (library_df,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(library_df, mo):
|
|
_out = (
|
|
mo.ui.dataframe(library_df)
|
|
if library_df is not None and library_df.height > 0
|
|
else mo.md("DataFrame is empty. Add some items first.")
|
|
)
|
|
_out
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md("""
|
|
## Summary
|
|
|
|
| Feature | Details |
|
|
|---------|---------|
|
|
| Storage | SQLite, 8 denormalized tables |
|
|
| Item types | rule, manual, regulation, download, source |
|
|
| Tags | `namespace:value` (module, source, year, rule, file, table) |
|
|
| Citations | APA (all types), Bluebook (regulations) |
|
|
| Translators | Federal Register, CMS Website, CMS Manual, eCFR, 4i |
|
|
| Source | `src/bib/` (mounted read-only via `PYTHONPATH`) |
|
|
|
|
The `bib` module is available at `from bib import connect, Store, Rule, Tag`.
|
|
""")
|
|
return
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run()
|