142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
"""The visual-feed job: read VISUAL (or the fixture files), record the read in the cache, build the candidates
|
|||
|
|
from the cache and apply them.
|
||
|
|
|
||
|
|
Per run: the feed settings; the tracked keys (every work order of a live VISUAL packet that is not filed, plus the
|
||
|
|
go-live rows Clerical pulled in); the window from today in site time; one read through the source (labor at most
|
||
|
|
every 30 minutes); `cache.write_batch`; `cache.rows_for_apply`; `candidates.build`; `apply.apply`. The run is
|
||
|
|
degraded when a read section failed or the batch is suspect.
|
||
|
|
|
||
|
|
VisualKit: one `Visual.from_env()` per run, closed after, and the stack guard checked against its settings before
|
||
|
|
the first read. Read-only: VisualKit enforces it.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import UTC, datetime, timedelta
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
from ... import guard
|
||
|
|
from ...feed import apply as feed_apply
|
||
|
|
from ...feed import cache, candidates, config
|
||
|
|
from ...feed.config import FeedSettings
|
||
|
|
from ...feed.source import FeedSource, FixtureSource, VisualKitSource
|
||
|
|
from ...feed.types import FeedData, WoKey
|
||
|
|
from . import Counts, JobContext, JobSpec
|
||
|
|
|
||
|
|
LABOR_EVERY = timedelta(minutes=30)
|
||
|
|
|
||
|
|
|
||
|
|
def tracked_keys(conn: Any) -> list[WoKey]:
|
||
|
|
"""Work orders of live VISUAL packets that are not filed, plus pulled-in go-live rows."""
|
||
|
|
rows = conn.execute(
|
||
|
|
"SELECT DISTINCT w.wo_base, w.lot, w.split, w.sub FROM packet_work_orders w JOIN packets p"
|
||
|
|
" ON p.id = w.packet_id WHERE p.removed_at IS NULL AND p.status <> 'filed' AND p.source_kind = 'visual'"
|
||
|
|
" UNION SELECT wo_base, lot, split, sub FROM visual_ignored_wos WHERE pulled_in_at IS NOT NULL"
|
||
|
|
).fetchall()
|
||
|
|
return sorted({WoKey(r[0], r[1], r[2], r[3]) for r in rows}, key=lambda k: k.as_tuple())
|
||
|
|
|
||
|
|
|
||
|
|
def resource_map(conn: Any) -> dict[str, str]:
|
||
|
|
return {r[0]: r[1] for r in conn.execute("SELECT resource, target FROM visual_resource_map")}
|
||
|
|
|
||
|
|
|
||
|
|
def _labor_due(conn: Any, fs: FeedSettings) -> bool:
|
||
|
|
if not fs.labor_tickets:
|
||
|
|
return False
|
||
|
|
last = cache.last_labor_read(conn)
|
||
|
|
return last is None or datetime.now(UTC) - last >= LABOR_EVERY
|
||
|
|
|
||
|
|
|
||
|
|
def _read(source: FeedSource, ctx: JobContext, tracked: list[WoKey], window: tuple[Any, Any], labor: bool) -> FeedData:
|
||
|
|
return source.read(
|
||
|
|
site=ctx.settings.site_id,
|
||
|
|
since=window[0],
|
||
|
|
until=window[1],
|
||
|
|
tracked=tracked,
|
||
|
|
with_materials=True,
|
||
|
|
with_orders=True,
|
||
|
|
with_labor=labor,
|
||
|
|
labor_since=None,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def run(ctx: JobContext) -> Counts:
|
||
|
|
conn = ctx.conn
|
||
|
|
fs = ctx.feed_settings
|
||
|
|
today = ctx.today_site()
|
||
|
|
window = config.window(fs, today)
|
||
|
|
tracked = tracked_keys(conn)
|
||
|
|
labor_due = _labor_due(conn, fs)
|
||
|
|
|
||
|
|
if ctx.settings.feed_source == "fixture":
|
||
|
|
fixture = FixtureSource(ctx.settings.fixture_dir)
|
||
|
|
describe = fixture.describe()
|
||
|
|
data = _read(fixture, ctx, tracked, window, labor_due)
|
||
|
|
else:
|
||
|
|
from visualkit import Visual
|
||
|
|
|
||
|
|
with Visual.from_env() as v:
|
||
|
|
# The stack guard against this run's own VisualKit settings, before the first read.
|
||
|
|
result = guard.check_stack(ctx.settings, v.settings)
|
||
|
|
ctx.visual_target = ctx.visual_target or result.target
|
||
|
|
vk = VisualKitSource(v)
|
||
|
|
describe = vk.describe()
|
||
|
|
data = _read(vk, ctx, tracked, window, labor_due)
|
||
|
|
for e in data.errors:
|
||
|
|
ctx.error(e)
|
||
|
|
|
||
|
|
batch = cache.write_batch(conn, ctx.run_id, data, describe, window)
|
||
|
|
if batch.suspect:
|
||
|
|
ctx.degrade(f"suspect read: {batch.reason}; nothing marked gone")
|
||
|
|
|
||
|
|
in_play = {r.key for r in data.rows} | set(tracked)
|
||
|
|
rows, mats, mat_legs = cache.rows_for_apply(conn, in_play)
|
||
|
|
if data.material_legs:
|
||
|
|
mat_legs |= data.material_legs
|
||
|
|
built = candidates.build(
|
||
|
|
rows,
|
||
|
|
mats,
|
||
|
|
mat_legs,
|
||
|
|
resource_map=resource_map(conn),
|
||
|
|
window=window,
|
||
|
|
golive=fs.golive_date,
|
||
|
|
departments=fs.departments,
|
||
|
|
tracked_keys=set(tracked),
|
||
|
|
today=today,
|
||
|
|
)
|
||
|
|
bases = {r.wo for r in rows}
|
||
|
|
projects = {candidates.project_of(b) for b in bases} | {c.project for c in built.candidates.values()}
|
||
|
|
orders = cache.orders(conn, bases=bases | projects, projects=projects)
|
||
|
|
counts = feed_apply.apply(
|
||
|
|
conn,
|
||
|
|
ctx=ctx,
|
||
|
|
batch=batch,
|
||
|
|
feed_settings=fs,
|
||
|
|
built=built,
|
||
|
|
orders=orders,
|
||
|
|
labor=cache.labor_hours(conn),
|
||
|
|
today=today,
|
||
|
|
seen={r.key: r.st for r in data.rows},
|
||
|
|
missing=() if batch.suspect else data.tracked_missing,
|
||
|
|
)
|
||
|
|
counts["wos_window"] = data.window_count
|
||
|
|
counts["wos_tracked"] = len(tracked)
|
||
|
|
counts["rows"] = len(data.rows)
|
||
|
|
counts["materials"] = len(data.materials) if data.materials is not None else 0
|
||
|
|
counts["orders"] = int(batch.counts.get("orders", 0))
|
||
|
|
counts["labor_legs"] = int(batch.counts.get("labor_legs", 0))
|
||
|
|
counts["batch_id"] = batch.id
|
||
|
|
if batch.suspect:
|
||
|
|
counts["suspect_reason"] = batch.reason
|
||
|
|
cache.finish_batch(conn, batch.id, counts)
|
||
|
|
return counts
|
||
|
|
|
||
|
|
|
||
|
|
def _cadence(fs: FeedSettings, now_site: datetime) -> int | None:
|
||
|
|
return config.interval_seconds(fs, now_site)
|
||
|
|
|
||
|
|
|
||
|
|
SPEC = JobSpec(name="visual-feed", run=run, cadence=_cadence, pausable=True, chain=("drawing-lookup",))
|
||
|
|
|
||
|
|
__all__ = ["SPEC", "resource_map", "run", "tracked_keys"]
|