Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/ontology/adapters/onet.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/ontology/adapters/onet.go |
| Original SHA-256 | 0e864cbc103dc108d3518a532e2df3f8828389cb0e0fb2b9eca6851cbaeafa5a |
| Git blob SHA | 65fb41568eee65c3f3cacd4bd2043da035159db7 |
| Reading-copy SHA-256 | fea5efa50c4270190453c9d1e257b512988d3bc091679e7e09b63e9580750507 |
Every recorded occurrence
These 20 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | collision-deconfliction | 4ee5a09972608ed89f8377c6665542d5f450f0a6 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws6-retrieval | 6c56e29231ec05735927fa265c97d517499dfeaf | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | m5-price-to-win | 41000ad30fff4129fdaf5ec84e15ba14c27d4890 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws1-ontology-sync | b596f449a32a1016fbd403c36505cc00a1ac9ee6 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | master | 1e9249840a4302ec82e123b9c9cb6f75badea3e4 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws8-teaching-loop | c823f0fb6bb2aba0f39001eb6de96f4000d9321f | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | mag-substrate | b82c7100a492dec67dfd1eda6b3f53def859be35 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws3-clearance | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | phase2-eval-fairness | d361af254701975fe239927656ababb56374655e | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | time-to-fill-survival | b66e1e505863d4f17741a4b9ede73509e45bd6c6 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | graphrag-foundation | b0ddeb9f633fd138d09309d1a17b7d13bc3a67f7 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws9-bench-ingestion | f585a9ef32fa554663ce1fba9a414a54b6e49cf0 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws2-alias-relationship | e4dada4601c6eb101830f9d223a9655dfa120616 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | presentation-readiness | eb574acaa29d71ce4d0f852572e87c275c79fdb7 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | vendor-intelligence | fe9796a19c261951d3f2565206c0c1b5ac294465 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | ws7-federal-signal | 6a9f6ce9cfb13345ffb47770d4960e7c2d9c17c2 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | phase2-connectors | e038d4c687aec16f8bd9bc4d907eea14d91f8704 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
| Janady13/missionmatch / internal/modules/ontology/adapters/onet.go | capture-readiness | bae6af7cb2997e819174a9e9cfff5e16067a1975 | Open original |
Recovered source transcript
Download complete source text ↓404 lines · 14.4 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
package adapters
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/missionworkforce/missionmatch/internal/modules/ontology"
)
// ONETAttribution is the required attribution string for O*NET Web Services content.
// O*NET Web Services requires this notice on any product using their data.
// The exact text must be reproduced verbatim including its internal punctuation.
const ONETAttribution = "O*NET Web Services by the U.S. Department of Labor, Employment and Training Administration (USDOL/ETA). Used under the CC BY 4.0 license. O*NET is a trademark of USDOL/ETA."
// onetBaseURL is the O*NET Web Services v2.0 base URL. It is the default used by NewONETSource
// and can be overridden for tests by setting the baseURL field directly after construction.
const onetBaseURL = "https://api-v2.onetcenter.org"
// onetMaxRetries is the bounded number of retry attempts on a 429 response before giving up
// and returning ontology.ErrThrottled.
const onetMaxRetries = 3
// onetRetryDelay is the back off duration applied between 429 retry attempts (about 200ms).
const onetRetryDelay = 200 * time.Millisecond
// onetMinInterval is the minimum time between requests, enforcing a ceiling of 5 requests
// per second without requiring an external dependency.
const onetMinInterval = 200 * time.Millisecond
// ONETSource ingests the O*NET Web Services v2.0 occupation skills, knowledge, abilities, and
// technology skills into the ontology store. Skills are yielded as KindSkill records; knowledge
// and abilities are yielded as KindKSA records; technology skills (concrete software tools) are
// yielded as KindSkill records. Each element kind is globally deduped: a skill element such as
// Active Listening appears once no matter how many occupations list it, and a technology tool
// such as Apache Kafka appears once regardless of how many occupations reference it.
//
// This adapter is DORMANT until an ONET_API_KEY is [credential removed] The CLI constructs it only when
// the key is present. The unit tests cover the pure parser functions only and do not call the
// live API.
//
// Attribution: this adapter incorporates information from O*NET Web Services by the U.S.
// Department of Labor (see ONETAttribution for the full required notice).
type ONETSource struct {
apiKey string
baseURL string
client *http.Client
lastCall time.Time
}
// NewONETSource builds the adapter. apiKey is the O*NET developer key; it is sent only in the
// X-API-Key request header, never in the query string or body. client is the HTTP client; pass
// nil for a sensible 30 second default. The base URL defaults to the O*NET v2.0 production
// endpoint and can be overridden by setting the baseURL field after construction.
func NewONETSource(apiKey string, client *http.Client) *ONETSource {
if client == nil {
client = &http.Client{Timeout: 30 * time.Second}
}
return &ONETSource{
apiKey: [credential removed],
baseURL: onetBaseURL,
client: client,
}
}
// Name is the provenance label stamped on every record this source yields.
func (s *ONETSource) Name() string { return "O*NET Web Services" }
// Fetch lists all O*NET occupations, then for each occupation pulls skills, knowledge, abilities,
// and technology skills, yielding one VocabRecord per distinct element. A global seen map ensures
// each unique SourceRecordID is yielded exactly once: generic competencies (skills, knowledge,
// abilities) and technology tools that appear across many occupations are stored as one row, not
// one row per occupation. Requests are throttled to about 5 per second. A 429 that cannot be
// recovered within onetMaxRetries attempts returns ontology.ErrThrottled.
//
// This method incorporates information from O*NET Web Services by the U.S. Department of Labor.
func (s *ONETSource) Fetch(ctx context.Context, yield func(ontology.VocabRecord) error) error {
log.Print("onet: fetch started, incorporates information from O*NET Web Services by the U.S. Department of Labor")
codes, err := s.listOccupations(ctx)
if err != nil {
return fmt.Errorf("onet: list occupations: %w", err)
}
type elementSpec struct {
path string
category string
kind ontology.TermKind
isTech bool
}
specs := []elementSpec{
{path: "skills", category: "skill", kind: ontology.KindSkill},
{path: "knowledge", category: "knowledge", kind: ontology.KindKSA},
{path: "abilities", category: "ability", kind: ontology.KindKSA},
{path: "technology_skills", kind: ontology.KindSkill, isTech: true},
}
// seen tracks SourceRecordIDs that have already been yielded. Any record whose key appears
// in seen is skipped so the store receives one upsert per distinct competency or tool, not
// one per occupation that mentions it (roughly 1016 repetitions for common elements).
seen := make(map[string]bool)
for _, code := range codes {
for _, spec := range specs {
url := fmt.Sprintf("%s/online/occupations/%s/summary/%s", s.baseURL, code, spec.path)
data, err := s.getWithRetry(ctx, url)
if err != nil {
return fmt.Errorf("onet: occupation %s %s: %w", code, spec.path, err)
}
var recs []ontology.VocabRecord
if spec.isTech {
recs, err = parseONETTechSkills(data, code)
} else {
recs, err = parseONETElement(data, code, spec.category, spec.kind)
}
if err != nil {
return fmt.Errorf("onet: parse occupation %s %s: %w", code, spec.path, err)
}
for _, r := range recs {
if seen[r.SourceRecordID] {
continue
}
seen[r.SourceRecordID] = true
if err := yield(r); err != nil {
return err
}
}
}
}
return nil
}
// listOccupations fetches all pages of /online/occupations/ and returns the O*NET SOC codes.
func (s *ONETSource) listOccupations(ctx context.Context) ([]string, error) {
var codes []string
start := 1
for {
url := fmt.Sprintf("%s/online/occupations/?start=%d&end=%d", s.baseURL, start, start+99)
data, err := s.getWithRetry(ctx, url)
if err != nil {
return nil, err
}
page, err := parseONETOccupations(data)
if err != nil {
return nil, err
}
if len(page) == 0 {
break
}
codes = append(codes, page...)
var pag onetPagination
if err := json.Unmarshal(data, &pag); err != nil {
return nil, fmt.Errorf("onet: decode pagination: %w", err)
}
if pag.Next == "" || pag.End >= pag.Total {
break
}
start = pag.End + 1
}
return codes, nil
}
// throttle enforces a minimum interval between requests (about 5 per second) using a simple
// time.Since check and time.Sleep. No external package is needed. It respects ctx cancellation.
func (s *ONETSource) throttle(ctx context.Context) error {
elapsed := time.Since(s.lastCall)
if elapsed < onetMinInterval {
wait := onetMinInterval - elapsed
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(wait):
}
}
s.lastCall = time.Now()
return nil
}
// getWithRetry performs a GET with the X-API-Key header, respects the rate limiter, and retries
// up to onetMaxRetries times on a 429 response. If still throttled after all retries it returns
// ontology.ErrThrottled.
func (s *ONETSource) getWithRetry(ctx context.Context, url string) ([]byte, error) {
var lastStatus int
for attempt := 0; attempt <= onetMaxRetries; attempt++ {
if attempt > 0 {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(onetRetryDelay):
}
}
if err := s.throttle(ctx); err != nil {
return nil, fmt.Errorf("onet: throttle: %w", err)
}
data, status, err := s.get(ctx, url)
if err != nil {
return nil, err
}
if status == http.StatusTooManyRequests {
lastStatus = status
continue
}
if status != http.StatusOK {
return nil, fmt.Errorf("onet: provider returned status %d for %s", status, url)
}
return data, nil
}
if lastStatus == http.StatusTooManyRequests {
return nil, ontology.ErrThrottled
}
return nil, fmt.Errorf("onet: exhausted retries for %s", url)
}
// get performs one HTTP GET with the X-API-Key header. The key is sent ONLY in the header,
// never in the query string or body, as required by the O*NET Web Services terms of service.
func (s *ONETSource) get(ctx context.Context, url string) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, 0, fmt.Errorf("onet: new request: %w", err)
}
req.Header.Set("X-API-Key", s.apiKey)
resp, err := s.client.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("onet: do request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, fmt.Errorf("onet: read body: %w", err)
}
return body, resp.StatusCode, nil
}
// onetPagination holds the pagination envelope shared across O*NET v2.0 list responses.
type onetPagination struct {
Start int `json:"start"`
End int `json:"end"`
Total int `json:"total"`
Next string `json:"next"`
}
// onetOccupationsResponse mirrors the O*NET v2.0 occupations list response:
//
// GET /online/occupations/
// { occupation: [{ code, title, href, ... }], start, end, total, next }
//
// Confirmed from https://services.onetcenter.org/reference/online/browse/occupations.
type onetOccupationsResponse struct {
onetPagination
Occupation []struct {
Code string `json:"code"`
Title string `json:"title"`
} `json:"occupation"`
}
// parseONETOccupations decodes an O*NET occupations list response and returns the SOC codes.
// It is pure (no I/O) so it is directly unit testable.
func parseONETOccupations(data []byte) ([]string, error) {
var doc onetOccupationsResponse
if err := json.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("onet: decode occupations: %w", err)
}
codes := make([]string, 0, len(doc.Occupation))
for _, occ := range doc.Occupation {
c := strings.TrimSpace(occ.Code)
if c != "" {
codes = append(codes, c)
}
}
return codes, nil
}
// onetElementResponse mirrors the O*NET v2.0 per occupation skills, knowledge, and abilities
// summary response shape:
//
// GET /online/occupations/{code}/summary/{skills|knowledge|abilities}
// { element: [{ id, name, description, related }], start, end, total, next }
//
// Confirmed from https://services.onetcenter.org/reference/online/occupation/summary/skills,
// https://services.onetcenter.org/reference/online/occupation/summary/knowledge, and
// https://services.onetcenter.org/reference/online/occupation/summary/abilities.
// The "id" field is the O*NET content model element id (e.g. "2.A.1.b" for Active Listening).
// The "name" field is the human readable element name.
type onetElementResponse struct {
onetPagination
Element []struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
} `json:"element"`
}
// parseONETElement decodes a per occupation skills, knowledge, or abilities response and returns
// one VocabRecord per element. SourceRecordID uses the global "onet:<elementID>" scheme so the
// same competency from different occupations carries the same key, enabling dedup in Fetch via the
// seen map. category is the element type label ("skill", "knowledge", "ability"). kind is the
// TermKind to stamp on each record. It is pure (no I/O) so it is directly unit testable.
func parseONETElement(data []byte, socCode, category string, kind ontology.TermKind) ([]ontology.VocabRecord, error) {
var doc onetElementResponse
if err := json.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("onet: decode %s elements: %w", category, err)
}
out := make([]ontology.VocabRecord, 0, len(doc.Element))
for _, el := range doc.Element {
id := strings.TrimSpace(el.ID)
name := strings.TrimSpace(el.Name)
if id == "" || name == "" {
continue
}
raw, _ := json.Marshal(map[string]string{
"soc_code": socCode,
"element_id": id,
"name": name,
"description": el.Description,
"category": category,
})
out = append(out, ontology.VocabRecord{
SourceName: "O*NET Web Services",
SourceRecordID: "onet:" + id,
Kind: kind,
Term: name,
Code: id,
Category: category,
Description: strings.TrimSpace(el.Description),
Raw: raw,
})
}
return out, nil
}
// onetTechSkillsResponse mirrors the O*NET v2.0 per occupation technology_skills summary response:
//
// GET /online/occupations/{code}/summary/technology_skills
// { start, end, total, next, category: [{ code, title, example: [{ title, hot_technology }] }] }
//
// Confirmed from https://services.onetcenter.org/reference/online/occupation/summary/technology_skills.
// Each category groups a class of software tools; each example is a concrete product title.
// The response paginates via the shared "next" / "end" / "total" envelope.
type onetTechSkillsResponse struct {
onetPagination
Category []struct {
Code int `json:"code"`
Title string `json:"title"`
Example []struct {
Title string `json:"title"`
HotTechnology bool `json:"hot_technology"`
} `json:"example"`
} `json:"category"`
}
// parseONETTechSkills decodes a per occupation technology_skills response and returns one KindSkill
// VocabRecord per example title. SourceRecordID uses "onet-tech:" plus the lowercased, trimmed
// example title so the same tool (e.g. "Apache Kafka") from different occupations carries the same
// key and is deduped by the seen map in Fetch. Category is set to the tool category title (e.g.
// "Development environment software"). Code is empty (tech skills have no O*NET content model id).
// Raw is a compact JSON object carrying the SOC code, category title, and tool title.
// It is pure (no I/O) so it is directly unit testable.
func parseONETTechSkills(data []byte, socCode string) ([]ontology.VocabRecord, error) {
var doc onetTechSkillsResponse
if err := json.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("onet: decode technology_skills: %w", err)
}
var out []ontology.VocabRecord
for _, cat := range doc.Category {
catTitle := strings.TrimSpace(cat.Title)
for _, ex := range cat.Example {
title := strings.TrimSpace(ex.Title)
if title == "" {
continue
}
key := "onet-tech:" + strings.ToLower(title)
raw, _ := json.Marshal(map[string]string{
"soc_code": socCode,
"category": catTitle,
"title": title,
})
out = append(out, ontology.VocabRecord{
SourceName: "O*NET Web Services",
SourceRecordID: key,
Kind: ontology.KindSkill,
Term: title,
Code: "",
Category: catTitle,
Description: "",
Raw: raw,
})
}
}
return out, nil
}