@toolpath/tool-scraper 0.1.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -11
- package/dist/columns.d.ts +64 -0
- package/dist/columns.js +68 -0
- package/dist/conventions.d.ts +107 -15
- package/dist/conventions.js +126 -15
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +30 -0
- package/dist/families/emuge.d.ts +185 -0
- package/dist/families/emuge.js +163 -0
- package/dist/families/harvey.d.ts +1782 -0
- package/dist/families/harvey.js +1328 -0
- package/dist/families/index.js +5 -2
- package/dist/families/kennametal.d.ts +21 -0
- package/dist/families/kennametal.js +10 -0
- package/dist/families/maritool.d.ts +120 -0
- package/dist/families/maritool.js +175 -0
- package/dist/family.d.ts +35 -1
- package/dist/family.js +28 -0
- package/dist/identity.d.ts +18 -0
- package/dist/identity.js +46 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/measure.d.ts +112 -0
- package/dist/measure.js +130 -0
- package/dist/node/cli.d.ts +3 -0
- package/dist/node/cli.js +158 -3
- package/dist/node/main.js +0 -0
- package/dist/records.d.ts +214 -12
- package/dist/records.js +140 -5
- package/dist/registry.d.ts +56 -1
- package/dist/registry.js +79 -2
- package/dist/scrape.d.ts +15 -0
- package/dist/scrape.js +24 -0
- package/dist/vendors/destinytool/records.d.ts +13 -3
- package/dist/vendors/destinytool/records.js +49 -37
- package/dist/vendors/emuge/index.d.ts +17 -0
- package/dist/vendors/emuge/index.js +17 -0
- package/dist/vendors/emuge/records.d.ts +230 -0
- package/dist/vendors/emuge/records.js +543 -0
- package/dist/vendors/emuge/scrape.d.ts +227 -0
- package/dist/vendors/emuge/scrape.js +358 -0
- package/dist/vendors/emuge/value.d.ts +75 -0
- package/dist/vendors/emuge/value.js +116 -0
- package/dist/vendors/harvey/catalog.d.ts +53 -0
- package/dist/vendors/harvey/catalog.js +120 -0
- package/dist/vendors/harvey/header.d.ts +89 -0
- package/dist/vendors/harvey/header.js +185 -0
- package/dist/vendors/harvey/index.d.ts +21 -0
- package/dist/vendors/harvey/index.js +21 -0
- package/dist/vendors/harvey/lexicon.d.ts +73 -0
- package/dist/vendors/harvey/lexicon.js +126 -0
- package/dist/vendors/harvey/literal.d.ts +68 -0
- package/dist/vendors/harvey/literal.js +214 -0
- package/dist/vendors/harvey/records.d.ts +79 -0
- package/dist/vendors/harvey/records.js +163 -0
- package/dist/vendors/harvey/scrape.d.ts +187 -0
- package/dist/vendors/harvey/scrape.js +483 -0
- package/dist/vendors/harvey/value.d.ts +74 -0
- package/dist/vendors/harvey/value.js +119 -0
- package/dist/vendors/kennametal/family.d.ts +119 -0
- package/dist/vendors/kennametal/family.js +155 -0
- package/dist/vendors/kennametal/index.d.ts +1 -0
- package/dist/vendors/kennametal/index.js +1 -0
- package/dist/vendors/kennametal/records.d.ts +29 -4
- package/dist/vendors/kennametal/records.js +93 -26
- package/dist/vendors/kennametal/scrape.d.ts +25 -2
- package/dist/vendors/kennametal/scrape.js +28 -3
- package/dist/vendors/maritool/catalog.d.ts +81 -0
- package/dist/vendors/maritool/catalog.js +132 -0
- package/dist/vendors/maritool/index.d.ts +10 -0
- package/dist/vendors/maritool/index.js +10 -0
- package/dist/vendors/maritool/scrape.d.ts +297 -0
- package/dist/vendors/maritool/scrape.js +593 -0
- package/dist/vendors/regofix/scrape.d.ts +8 -11
- package/dist/vendors/regofix/scrape.js +21 -36
- package/package.json +31 -7
package/README.md
CHANGED
|
@@ -17,26 +17,41 @@ pnpm add @toolpath/tool-scraper
|
|
|
17
17
|
|
|
18
18
|
## Vendors
|
|
19
19
|
|
|
20
|
-
| Vendor | Transport
|
|
21
|
-
| ------------------ |
|
|
22
|
-
| Kennametal / WIDIA | AEM variant-table GET, parsed with `htmlparser2`
|
|
23
|
-
| REGO-FIX | Elasticsearch proxy POST + per-part DIN 4000 XML
|
|
24
|
-
| Destiny Tool | Firestore REST, paginated
|
|
20
|
+
| Vendor | Transport | What it publishes |
|
|
21
|
+
| ------------------ | ------------------------------------------------------- | ------------------------------------ |
|
|
22
|
+
| Kennametal / WIDIA | AEM variant-table GET, parsed with `htmlparser2` | tools and toolholding |
|
|
23
|
+
| REGO-FIX | Elasticsearch proxy POST + per-part DIN 4000 XML | toolholding |
|
|
24
|
+
| Destiny Tool | Firestore REST, paginated | solid end mills |
|
|
25
|
+
| Harvey Tool | inline JS literal on a product page, plus its `<thead>` | miniature end mills, keyseat cutters |
|
|
26
|
+
| MariTool | osCommerce category listings, then one page per part | toolholding |
|
|
27
|
+
| EMUGE-FRANKEN | SAP Commerce JSON API: grouped, variant, batched detail | end mills, twist drills, taps |
|
|
25
28
|
|
|
26
29
|
## Two entry points
|
|
27
30
|
|
|
28
|
-
**`@toolpath/tool-scraper` returns records.** Every scrape hands back
|
|
29
|
-
say where they came from;
|
|
30
|
-
|
|
31
|
+
**`@toolpath/tool-scraper` returns records.** Every scrape hands back the vendor's own rows and
|
|
32
|
+
enough provenance to say where they came from; `toRecords` turns one family's scrape into
|
|
33
|
+
`ToolRecord[]` — canonical ISO 13399 geometry, one shape whatever the vendor. Nothing in either
|
|
34
|
+
touches the filesystem, so a backend can embed it and do what it likes with the result.
|
|
31
35
|
|
|
32
36
|
```ts
|
|
33
|
-
import { createFetcher } from '@toolpath/tool-scraper'
|
|
37
|
+
import { createFetcher, type ToolRecord } from '@toolpath/tool-scraper'
|
|
38
|
+
import { toRecords } from '@toolpath/tool-scraper/registry'
|
|
34
39
|
import { scrapeFamily } from '@toolpath/tool-scraper/vendors/kennametal'
|
|
35
40
|
|
|
36
41
|
const fetcher = createFetcher() // or your own: retries, proxy, rate limits
|
|
37
|
-
const
|
|
42
|
+
const scrape = await scrapeFamily(fetcher, '100003658')
|
|
43
|
+
|
|
44
|
+
const records: ToolRecord[] = toRecords('godrill_3xd_metric.csv', scrape)
|
|
45
|
+
// { brand: 'kennametal', guid: '…', geometry: { DC: 10, OAL: 89, … },
|
|
46
|
+
// materialGroups: ['P', 'N'], materialGroupsSource: 'vendor-stated', … }
|
|
38
47
|
```
|
|
39
48
|
|
|
49
|
+
`toRecords` is on the `./registry` subpath because it is the one place that knows both the family
|
|
50
|
+
table and the vendor adapters; the main entry point deliberately imports no vendor. It checks the
|
|
51
|
+
scrape's header for the identity and mapped columns before it maps a single row, so a re-scrape
|
|
52
|
+
whose part-number column was renamed fails by name instead of minting every guid off an empty
|
|
53
|
+
string.
|
|
54
|
+
|
|
40
55
|
The transport is a parameter, not a module global. Supply your own `Fetcher` and the vendor
|
|
41
56
|
adapters read through it — which is also how every test in this package runs without a network.
|
|
42
57
|
|
|
@@ -57,6 +72,11 @@ toolpath-scrape kennametal 100003658 "$TOOLPATH_SCRAPE_ROOT/kennametal/csv/godri
|
|
|
57
72
|
toolpath-scrape materials godrill_3xd_metric.csv
|
|
58
73
|
toolpath-scrape regofix holders "$TOOLPATH_SCRAPE_ROOT/regofix/csv/regofix_bt30_pg_holders.csv"
|
|
59
74
|
toolpath-scrape destinytool "$TOOLPATH_SCRAPE_ROOT/destinytool/csv/destinytool_end_mills_inch.csv"
|
|
75
|
+
toolpath-scrape harvey harvey_endmill_008.csv # the page and the unit come from its config
|
|
76
|
+
toolpath-scrape harvey --catalog # what the four category trees link to today
|
|
77
|
+
toolpath-scrape maritool maritool_cat40_holders.csv # its leaf categories come from its config
|
|
78
|
+
toolpath-scrape maritool --catalog # what the five taper trees hold today
|
|
79
|
+
toolpath-scrape emuge emuge_drills.csv # its category and unit come from its config
|
|
60
80
|
```
|
|
61
81
|
|
|
62
82
|
`toolpath-scrape --help` lists the rest.
|
|
@@ -71,6 +91,14 @@ definition and names the three that are Autodesk's rather than the standard's.
|
|
|
71
91
|
Vendor CSVs keep the **vendor's** own column labels. Nothing reads a vendor's CSV but that vendor's
|
|
72
92
|
adapter, and `conventions.ts` holds the short list of rules that do hold across all of them.
|
|
73
93
|
|
|
94
|
+
`materialGroups` has three states, and they are different claims: `null` labelled `unspecified` is
|
|
95
|
+
"we do not know what this tool is for" — not indexed, not published, or not swept — `[]` is a vendor
|
|
96
|
+
index that rates the part for nothing, and a non-empty list is a rating. `materialGroupsSource` is
|
|
97
|
+
never absent: it is `unspecified`, or it says whether the rating was `vendor-stated` or `derived`
|
|
98
|
+
here. Every Harvey record is `unspecified`: Harvey's material index is published per part rather
|
|
99
|
+
than in a variant table, and a scrape cannot reach it — see
|
|
100
|
+
[`docs/HARVEY_PRODUCT_TABLE.md`](docs/HARVEY_PRODUCT_TABLE.md) §1.5.1.
|
|
101
|
+
|
|
74
102
|
Every per-family constant no vendor table states carries its provenance — whether it was
|
|
75
103
|
vendor-stated, derived or assumed, and by whom on what date. The types enforce it: an assumed fact
|
|
76
104
|
without a note, a date and initials does not compile.
|
|
@@ -80,7 +108,9 @@ without a note, a date and initials does not compile.
|
|
|
80
108
|
- [`docs/ADDING-A-VENDOR.md`](docs/ADDING-A-VENDOR.md) — the runbook.
|
|
81
109
|
- [`docs/KENNAMETAL_CAD_API.md`](docs/KENNAMETAL_CAD_API.md),
|
|
82
110
|
[`docs/KENNAMETAL_SPEEDFEED_API.md`](docs/KENNAMETAL_SPEEDFEED_API.md),
|
|
83
|
-
[`docs/REGOFIX_PRODUCTFINDER_API.md`](docs/REGOFIX_PRODUCTFINDER_API.md)
|
|
111
|
+
[`docs/REGOFIX_PRODUCTFINDER_API.md`](docs/REGOFIX_PRODUCTFINDER_API.md),
|
|
112
|
+
[`docs/HARVEY_PRODUCT_TABLE.md`](docs/HARVEY_PRODUCT_TABLE.md),
|
|
113
|
+
[`docs/MARITOOL_CATALOG.md`](docs/MARITOOL_CATALOG.md) — how each endpoint or table was
|
|
84
114
|
found, and the dead ends tried first.
|
|
85
115
|
- [`../../docs/TOOL-SCRAPER-PLAN.md`](../../docs/TOOL-SCRAPER-PLAN.md) — the structure, the evidence
|
|
86
116
|
behind it, and what has landed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading one canonical field out of a scraped row, for every vendor whose CSV
|
|
3
|
+
* holds display strings rather than numbers.
|
|
4
|
+
*
|
|
5
|
+
* Two vendors publish a receipt of the vendor's own text — Harvey Tool and
|
|
6
|
+
* EMUGE-FRANKEN — and a mapper for either one needs the same three steps
|
|
7
|
+
* between a `GeometryName` and a number: ask the family's `ColumnMap` which
|
|
8
|
+
* column that field is in, read the cell, and decide what an unreadable one
|
|
9
|
+
* costs. Those three were a verbatim copy in both mappers, down to the wording
|
|
10
|
+
* of the refusal, which is the same argument `measure.ts` makes one level down:
|
|
11
|
+
* two copies of four lines is cheap and two copies of the **decision** is not.
|
|
12
|
+
*
|
|
13
|
+
* ## What is shared and what is not
|
|
14
|
+
*
|
|
15
|
+
* The vendor supplies the grammar, as a {@link LengthReader} — Harvey's
|
|
16
|
+
* `dimension` reads `.250 (1/4)` and a `-` that means "does not apply", EMUGE's
|
|
17
|
+
* `measureIn` reads `1 1/2 "` and refuses a range. What this module owns is
|
|
18
|
+
* everything either side of that call:
|
|
19
|
+
*
|
|
20
|
+
* - a column the family maps to nothing is `undefined`, not an error — a family
|
|
21
|
+
* with no neck column is a plain tool, and that is the mapper's fallback to
|
|
22
|
+
* make;
|
|
23
|
+
* - a **required** field with no reading refuses the row with an
|
|
24
|
+
* `errors.IncompletePartError`, naming the canonical field and quoting the
|
|
25
|
+
* cell, because a tool with no cutting diameter is not a part — and that is
|
|
26
|
+
* the one refusal `registry.toRecords` skips past rather than failing the
|
|
27
|
+
* whole family on;
|
|
28
|
+
* - an **optional** one answers null and lets the mapper decide.
|
|
29
|
+
*
|
|
30
|
+
* `vendors/destinytool/records.ts` keeps its own `required` and is not wired
|
|
31
|
+
* through here: it reads a dimension out of free text rather than out of a
|
|
32
|
+
* cell, throws `RangeError` from its own parser rather than answering null, and
|
|
33
|
+
* has no `optional` at all. Forcing it into this shape would change how it
|
|
34
|
+
* refuses, which is the one thing `measure.ts` says is a vendor's own call.
|
|
35
|
+
*/
|
|
36
|
+
import type { UnitSystem } from './conventions.js';
|
|
37
|
+
import type { ColumnMap, GeometryName } from './records.js';
|
|
38
|
+
import type { MapperOptions, ScrapedRow, Warn } from './scrape.js';
|
|
39
|
+
/**
|
|
40
|
+
* How one vendor turns its own display cell into a length in `unit`.
|
|
41
|
+
*
|
|
42
|
+
* The signature `vendors/harvey/value.ts`'s `dimension` and
|
|
43
|
+
* `vendors/emuge/value.ts`'s `measureIn` already have.
|
|
44
|
+
*/
|
|
45
|
+
export type LengthReader = (display: string, unit: UnitSystem, what: string, warn?: Warn) => number | null;
|
|
46
|
+
/** The three readers a display-string mapper needs, bound to one grammar. */
|
|
47
|
+
export interface ColumnReaders {
|
|
48
|
+
/** One canonical field's cell, or undefined where the family maps none. */
|
|
49
|
+
cell(row: ScrapedRow, columns: ColumnMap, canonical: GeometryName, unit: UnitSystem): string | undefined;
|
|
50
|
+
/** A dimension the kind requires, refusing a row the vendor left blank. */
|
|
51
|
+
required(row: ScrapedRow, columns: ColumnMap, canonical: GeometryName, unit: UnitSystem, what: string, options: MapperOptions): number;
|
|
52
|
+
/** A dimension the contract does not require. Null where there is none. */
|
|
53
|
+
optional(row: ScrapedRow, columns: ColumnMap, canonical: GeometryName, unit: UnitSystem, what: string, options: MapperOptions): number | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The three readers, over one vendor's `read`.
|
|
57
|
+
*
|
|
58
|
+
* `columns` is the caller's map and not `family.columns`. They are the same
|
|
59
|
+
* object through `registry.toRecords` — but `RecordMapper` passes one as an
|
|
60
|
+
* argument, `registry` validates *that* one with `checkColumnsExist`, and a
|
|
61
|
+
* mapper reading a different reference is validating one map and reading
|
|
62
|
+
* another.
|
|
63
|
+
*/
|
|
64
|
+
export declare function columnReaders(read: LengthReader): ColumnReaders;
|
package/dist/columns.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading one canonical field out of a scraped row, for every vendor whose CSV
|
|
3
|
+
* holds display strings rather than numbers.
|
|
4
|
+
*
|
|
5
|
+
* Two vendors publish a receipt of the vendor's own text — Harvey Tool and
|
|
6
|
+
* EMUGE-FRANKEN — and a mapper for either one needs the same three steps
|
|
7
|
+
* between a `GeometryName` and a number: ask the family's `ColumnMap` which
|
|
8
|
+
* column that field is in, read the cell, and decide what an unreadable one
|
|
9
|
+
* costs. Those three were a verbatim copy in both mappers, down to the wording
|
|
10
|
+
* of the refusal, which is the same argument `measure.ts` makes one level down:
|
|
11
|
+
* two copies of four lines is cheap and two copies of the **decision** is not.
|
|
12
|
+
*
|
|
13
|
+
* ## What is shared and what is not
|
|
14
|
+
*
|
|
15
|
+
* The vendor supplies the grammar, as a {@link LengthReader} — Harvey's
|
|
16
|
+
* `dimension` reads `.250 (1/4)` and a `-` that means "does not apply", EMUGE's
|
|
17
|
+
* `measureIn` reads `1 1/2 "` and refuses a range. What this module owns is
|
|
18
|
+
* everything either side of that call:
|
|
19
|
+
*
|
|
20
|
+
* - a column the family maps to nothing is `undefined`, not an error — a family
|
|
21
|
+
* with no neck column is a plain tool, and that is the mapper's fallback to
|
|
22
|
+
* make;
|
|
23
|
+
* - a **required** field with no reading refuses the row with an
|
|
24
|
+
* `errors.IncompletePartError`, naming the canonical field and quoting the
|
|
25
|
+
* cell, because a tool with no cutting diameter is not a part — and that is
|
|
26
|
+
* the one refusal `registry.toRecords` skips past rather than failing the
|
|
27
|
+
* whole family on;
|
|
28
|
+
* - an **optional** one answers null and lets the mapper decide.
|
|
29
|
+
*
|
|
30
|
+
* `vendors/destinytool/records.ts` keeps its own `required` and is not wired
|
|
31
|
+
* through here: it reads a dimension out of free text rather than out of a
|
|
32
|
+
* cell, throws `RangeError` from its own parser rather than answering null, and
|
|
33
|
+
* has no `optional` at all. Forcing it into this shape would change how it
|
|
34
|
+
* refuses, which is the one thing `measure.ts` says is a vendor's own call.
|
|
35
|
+
*/
|
|
36
|
+
import { IncompletePartError } from './errors.js';
|
|
37
|
+
/**
|
|
38
|
+
* The three readers, over one vendor's `read`.
|
|
39
|
+
*
|
|
40
|
+
* `columns` is the caller's map and not `family.columns`. They are the same
|
|
41
|
+
* object through `registry.toRecords` — but `RecordMapper` passes one as an
|
|
42
|
+
* argument, `registry` validates *that* one with `checkColumnsExist`, and a
|
|
43
|
+
* mapper reading a different reference is validating one map and reading
|
|
44
|
+
* another.
|
|
45
|
+
*/
|
|
46
|
+
export function columnReaders(read) {
|
|
47
|
+
const cell = (row, columns, canonical, unit) => {
|
|
48
|
+
const column = columns.column(canonical, unit);
|
|
49
|
+
return column === null ? undefined : row[column];
|
|
50
|
+
};
|
|
51
|
+
const optional = (row, columns, canonical, unit, what, options) => {
|
|
52
|
+
const raw = cell(row, columns, canonical, unit);
|
|
53
|
+
return raw === undefined ? null : read(raw, unit, what, options.warn);
|
|
54
|
+
};
|
|
55
|
+
const required = (row, columns, canonical, unit, what, options) => {
|
|
56
|
+
const raw = cell(row, columns, canonical, unit);
|
|
57
|
+
const value = raw === undefined ? null : read(raw, unit, what, options.warn);
|
|
58
|
+
if (value === null) {
|
|
59
|
+
// `IncompletePartError` and not the general vendor fault: this is the
|
|
60
|
+
// one refusal `registry.toRecords` skips past, because a single part the
|
|
61
|
+
// vendor left a cell blank on must not end a family's conversion. See
|
|
62
|
+
// that type for why the others still must not be skipped.
|
|
63
|
+
throw new IncompletePartError(what, `publishes no ${canonical} — its cell is ${JSON.stringify(raw ?? '')}`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
};
|
|
67
|
+
return { cell, required, optional };
|
|
68
|
+
}
|
package/dist/conventions.d.ts
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
* *across* the CSVs anyway, and the reason to make them explicit is that
|
|
13
13
|
* vendor #3 already drifted from one:
|
|
14
14
|
*
|
|
15
|
-
* | Convention
|
|
16
|
-
* |
|
|
17
|
-
* | `_mm`/`_in` carries the unit on a dimension
|
|
18
|
-
* | Multi-value cells are space-separated
|
|
19
|
-
* | One row per orderable part
|
|
20
|
-
* | `CAD_STEP_URL` names a CAD model where one exists
|
|
21
|
-
* |
|
|
22
|
-
* |
|
|
15
|
+
* | Convention | Held by |
|
|
16
|
+
* | -------------------------------------------------- | ------------------------- |
|
|
17
|
+
* | `_mm`/`_in` carries the unit on a dimension | all five |
|
|
18
|
+
* | Multi-value cells are space-separated | all five |
|
|
19
|
+
* | One row per orderable part | all five |
|
|
20
|
+
* | `CAD_STEP_URL` names a CAD model where one exists | Kennametal, REGO-FIX |
|
|
21
|
+
* | `CAD_DXF_URL` names a 2D profile where one exists | Harvey, MariTool |
|
|
22
|
+
* | `Description` carries the vendor's own free text | Harvey, MariTool |
|
|
23
|
+
* | `contact` says how a holder seats | REGO-FIX, MariTool |
|
|
24
|
+
* | `CST` names the collet series a holder takes | REGO-FIX, MariTool |
|
|
25
|
+
* | `L1_in`/`L1_mm` carry a holder's gage length | REGO-FIX, MariTool |
|
|
26
|
+
* | Unmapped vendor codes keep a `DIN_` prefix | REGO-FIX; rule is general |
|
|
27
|
+
* | The identity columns | **broken** — see below |
|
|
23
28
|
*
|
|
24
29
|
* Identity and units are the two worth enforcing; the rest are advisory, and
|
|
25
30
|
* are here so that "advisory" is a decision on the page rather than an
|
|
@@ -63,6 +68,87 @@ export declare const UNIT_SUFFIX: Record<UnitSystem, string>;
|
|
|
63
68
|
* neither owns it — the leak the vendor-boundary test exists to catch.
|
|
64
69
|
*/
|
|
65
70
|
export declare const CAD_COLUMN = "CAD_STEP_URL";
|
|
71
|
+
/**
|
|
72
|
+
* The CSV column holding a part's downloadable 2D DXF profile.
|
|
73
|
+
*
|
|
74
|
+
* A second column rather than a second thing written into {@link CAD_COLUMN},
|
|
75
|
+
* because a DXF is not a STEP model: one is a flat profile a machinist prints
|
|
76
|
+
* or traces, the other is the solid a CAM system imports. Harvey Tool publishes
|
|
77
|
+
* a DXF for 12,773 of its 12,799 parts and a STEP for none of them, so writing
|
|
78
|
+
* its link into `CAD_STEP_URL` would repeat exactly the mistake the
|
|
79
|
+
* `CAD_STP_LWM` -> `CAD_STEP_URL` rename fixed on 2026-08-08 — a column name
|
|
80
|
+
* that is a claim about the data, and false.
|
|
81
|
+
*
|
|
82
|
+
* Vendor-neutral and beside `CAD_COLUMN` for the same reason that one is: a
|
|
83
|
+
* format is not one manufacturer's fact, and the second vendor to publish a DXF
|
|
84
|
+
* must write it into this column rather than inventing another.
|
|
85
|
+
*/
|
|
86
|
+
export declare const CAD_DXF_COLUMN = "CAD_DXF_URL";
|
|
87
|
+
/**
|
|
88
|
+
* The CSV column holding the vendor's own free text about one part.
|
|
89
|
+
*
|
|
90
|
+
* Harvey Tool writes a product title here and MariTool a product name; both are
|
|
91
|
+
* the vendor's own prose rather than a designation this package composed. It is
|
|
92
|
+
* here and not in either adapter for the reason {@link CAD_COLUMN} is: two of
|
|
93
|
+
* them write it and neither owns it.
|
|
94
|
+
*
|
|
95
|
+
* **Not every vendor publishes one, and `''` is the answer where none does** —
|
|
96
|
+
* a description that restates the catalog number is not a description. See
|
|
97
|
+
* `records.ToolRecord.description`, which states the same rule for the record.
|
|
98
|
+
*/
|
|
99
|
+
export declare const DESCRIPTION_COLUMN = "Description";
|
|
100
|
+
/**
|
|
101
|
+
* The CSV column holding the vendor's own full name for the family a part is
|
|
102
|
+
* in — `KenCut™ FF • HPFT • Square End • 6 Flutes • Plain Shank • Inch`.
|
|
103
|
+
*
|
|
104
|
+
* Constant down a family's whole table, and that is what makes it a *family*
|
|
105
|
+
* title rather than a {@link DESCRIPTION_COLUMN}: it names the group, not the
|
|
106
|
+
* part. Kennametal and WIDIA publish one as the `h1` of a family page and it
|
|
107
|
+
* reaches the CSV whole, while `records.ToolRecord.productLine` keeps only its
|
|
108
|
+
* leading segment — the rest is the vendor's own wording for the shape, the
|
|
109
|
+
* flute count, the shank and the unit, and a receipt that dropped it would be
|
|
110
|
+
* throwing away published text to save a column.
|
|
111
|
+
*
|
|
112
|
+
* Vendor-neutral and here rather than in the Kennametal adapter for the reason
|
|
113
|
+
* {@link CAD_COLUMN} is: a second vendor that publishes a family title writes
|
|
114
|
+
* this column rather than inventing another. Nothing forces one to — a vendor
|
|
115
|
+
* whose parts carry no family title simply has no such column.
|
|
116
|
+
*/
|
|
117
|
+
export declare const FAMILY_TITLE_COLUMN = "Family Title";
|
|
118
|
+
/**
|
|
119
|
+
* The CSV column saying how a holder seats in the spindle: `taper` or `face`.
|
|
120
|
+
*
|
|
121
|
+
* `face` is dual contact — the flange face seats at the same time as the cone.
|
|
122
|
+
* REGO-FIX resolves it from its own `form_name` and MariTool from its `Taper`
|
|
123
|
+
* cell, which is the right shape: **how** a vendor states it is that vendor's
|
|
124
|
+
* business, and what the column is called is not.
|
|
125
|
+
*/
|
|
126
|
+
export declare const CONTACT_COLUMN = "contact";
|
|
127
|
+
/**
|
|
128
|
+
* The CSV column naming the collet series a holder accepts — `ER16`, `PG25`.
|
|
129
|
+
*
|
|
130
|
+
* The join key between a holder family and a collet family, which is exactly
|
|
131
|
+
* why it cannot be spelled twice: `families/kennametal.ts` states the join
|
|
132
|
+
* against this column, and two spellings of it join to nothing. Both vendors
|
|
133
|
+
* that publish it close the vendor's own spacing before writing it here, for
|
|
134
|
+
* the same reason.
|
|
135
|
+
*/
|
|
136
|
+
export declare const COLLET_SERIES_COLUMN = "CST";
|
|
137
|
+
/**
|
|
138
|
+
* The CSV columns carrying a holder's gage length, one per unit system.
|
|
139
|
+
*
|
|
140
|
+
* A **pair** with exactly one cell filled, rather than one column and a unit
|
|
141
|
+
* tag, because a single catalog page can publish both: MariTool gages
|
|
142
|
+
* `HSK40E-ER11-40` in millimetres and `HSK40E-ER16-3.0M` in inches on one
|
|
143
|
+
* listing. Nothing is converted between them — the vendor's own imperial
|
|
144
|
+
* conversion is unusable and computing one here would put a number in the file
|
|
145
|
+
* the vendor never published.
|
|
146
|
+
*
|
|
147
|
+
* REGO-FIX fills only the millimetre cell, because its DIN 4000 documents are
|
|
148
|
+
* metric throughout; the pair is still the shape, so the two vendors' holder
|
|
149
|
+
* CSVs answer the same question with the same columns.
|
|
150
|
+
*/
|
|
151
|
+
export declare const GAGE_COLUMNS: Record<UnitSystem, string>;
|
|
66
152
|
/**
|
|
67
153
|
* The prefix an unmapped vendor code keeps, so it cannot read as a dimension.
|
|
68
154
|
*
|
|
@@ -85,13 +171,19 @@ export declare const IDENTITY_COLUMNS: readonly ["Material Number", "ISO Catalog
|
|
|
85
171
|
* Where a vendor's CSV does not use {@link IDENTITY_COLUMNS}, and what it uses
|
|
86
172
|
* instead.
|
|
87
173
|
*
|
|
88
|
-
* **
|
|
89
|
-
* adopted Kennametal's identity labels; Destiny Tool passes
|
|
90
|
-
* `itemNumber` straight through and publishes no catalog
|
|
91
|
-
* the convention was real but informal, and it eroded the
|
|
92
|
-
* did not resemble the first two. Writing the deviation
|
|
93
|
-
*
|
|
94
|
-
* happened.
|
|
174
|
+
* **Three entries, and they are a record of drift rather than a licence.**
|
|
175
|
+
* REGO-FIX adopted Kennametal's identity labels; Destiny Tool passes
|
|
176
|
+
* Firestore's own `itemNumber` straight through and publishes no catalog
|
|
177
|
+
* designation at all — the convention was real but informal, and it eroded the
|
|
178
|
+
* first time a vendor did not resemble the first two. Writing the deviation
|
|
179
|
+
* down is what makes the next vendor's drift a decision somebody made rather
|
|
180
|
+
* than a thing that happened.
|
|
181
|
+
*
|
|
182
|
+
* Harvey Tool and MariTool are the honest kind, and they are now the majority:
|
|
183
|
+
* both genuinely publish one identifier per part, so their entries record a
|
|
184
|
+
* fact about the vendor rather than a shortcut taken here. Two of the two
|
|
185
|
+
* vendors added since the convention was written have needed one, which says
|
|
186
|
+
* the two-column shape is Kennametal's rather than the industry's.
|
|
95
187
|
*/
|
|
96
188
|
export declare const IDENTITY_DEVIATIONS: Partial<Record<BrandName, readonly string[]>>;
|
|
97
189
|
/**
|
package/dist/conventions.js
CHANGED
|
@@ -12,14 +12,19 @@
|
|
|
12
12
|
* *across* the CSVs anyway, and the reason to make them explicit is that
|
|
13
13
|
* vendor #3 already drifted from one:
|
|
14
14
|
*
|
|
15
|
-
* | Convention
|
|
16
|
-
* |
|
|
17
|
-
* | `_mm`/`_in` carries the unit on a dimension
|
|
18
|
-
* | Multi-value cells are space-separated
|
|
19
|
-
* | One row per orderable part
|
|
20
|
-
* | `CAD_STEP_URL` names a CAD model where one exists
|
|
21
|
-
* |
|
|
22
|
-
* |
|
|
15
|
+
* | Convention | Held by |
|
|
16
|
+
* | -------------------------------------------------- | ------------------------- |
|
|
17
|
+
* | `_mm`/`_in` carries the unit on a dimension | all five |
|
|
18
|
+
* | Multi-value cells are space-separated | all five |
|
|
19
|
+
* | One row per orderable part | all five |
|
|
20
|
+
* | `CAD_STEP_URL` names a CAD model where one exists | Kennametal, REGO-FIX |
|
|
21
|
+
* | `CAD_DXF_URL` names a 2D profile where one exists | Harvey, MariTool |
|
|
22
|
+
* | `Description` carries the vendor's own free text | Harvey, MariTool |
|
|
23
|
+
* | `contact` says how a holder seats | REGO-FIX, MariTool |
|
|
24
|
+
* | `CST` names the collet series a holder takes | REGO-FIX, MariTool |
|
|
25
|
+
* | `L1_in`/`L1_mm` carry a holder's gage length | REGO-FIX, MariTool |
|
|
26
|
+
* | Unmapped vendor codes keep a `DIN_` prefix | REGO-FIX; rule is general |
|
|
27
|
+
* | The identity columns | **broken** — see below |
|
|
23
28
|
*
|
|
24
29
|
* Identity and units are the two worth enforcing; the rest are advisory, and
|
|
25
30
|
* are here so that "advisory" is a decision on the page rather than an
|
|
@@ -64,6 +69,90 @@ export const UNIT_SUFFIX = {
|
|
|
64
69
|
* neither owns it — the leak the vendor-boundary test exists to catch.
|
|
65
70
|
*/
|
|
66
71
|
export const CAD_COLUMN = 'CAD_STEP_URL';
|
|
72
|
+
/**
|
|
73
|
+
* The CSV column holding a part's downloadable 2D DXF profile.
|
|
74
|
+
*
|
|
75
|
+
* A second column rather than a second thing written into {@link CAD_COLUMN},
|
|
76
|
+
* because a DXF is not a STEP model: one is a flat profile a machinist prints
|
|
77
|
+
* or traces, the other is the solid a CAM system imports. Harvey Tool publishes
|
|
78
|
+
* a DXF for 12,773 of its 12,799 parts and a STEP for none of them, so writing
|
|
79
|
+
* its link into `CAD_STEP_URL` would repeat exactly the mistake the
|
|
80
|
+
* `CAD_STP_LWM` -> `CAD_STEP_URL` rename fixed on 2026-08-08 — a column name
|
|
81
|
+
* that is a claim about the data, and false.
|
|
82
|
+
*
|
|
83
|
+
* Vendor-neutral and beside `CAD_COLUMN` for the same reason that one is: a
|
|
84
|
+
* format is not one manufacturer's fact, and the second vendor to publish a DXF
|
|
85
|
+
* must write it into this column rather than inventing another.
|
|
86
|
+
*/
|
|
87
|
+
export const CAD_DXF_COLUMN = 'CAD_DXF_URL';
|
|
88
|
+
/**
|
|
89
|
+
* The CSV column holding the vendor's own free text about one part.
|
|
90
|
+
*
|
|
91
|
+
* Harvey Tool writes a product title here and MariTool a product name; both are
|
|
92
|
+
* the vendor's own prose rather than a designation this package composed. It is
|
|
93
|
+
* here and not in either adapter for the reason {@link CAD_COLUMN} is: two of
|
|
94
|
+
* them write it and neither owns it.
|
|
95
|
+
*
|
|
96
|
+
* **Not every vendor publishes one, and `''` is the answer where none does** —
|
|
97
|
+
* a description that restates the catalog number is not a description. See
|
|
98
|
+
* `records.ToolRecord.description`, which states the same rule for the record.
|
|
99
|
+
*/
|
|
100
|
+
export const DESCRIPTION_COLUMN = 'Description';
|
|
101
|
+
/**
|
|
102
|
+
* The CSV column holding the vendor's own full name for the family a part is
|
|
103
|
+
* in — `KenCut™ FF • HPFT • Square End • 6 Flutes • Plain Shank • Inch`.
|
|
104
|
+
*
|
|
105
|
+
* Constant down a family's whole table, and that is what makes it a *family*
|
|
106
|
+
* title rather than a {@link DESCRIPTION_COLUMN}: it names the group, not the
|
|
107
|
+
* part. Kennametal and WIDIA publish one as the `h1` of a family page and it
|
|
108
|
+
* reaches the CSV whole, while `records.ToolRecord.productLine` keeps only its
|
|
109
|
+
* leading segment — the rest is the vendor's own wording for the shape, the
|
|
110
|
+
* flute count, the shank and the unit, and a receipt that dropped it would be
|
|
111
|
+
* throwing away published text to save a column.
|
|
112
|
+
*
|
|
113
|
+
* Vendor-neutral and here rather than in the Kennametal adapter for the reason
|
|
114
|
+
* {@link CAD_COLUMN} is: a second vendor that publishes a family title writes
|
|
115
|
+
* this column rather than inventing another. Nothing forces one to — a vendor
|
|
116
|
+
* whose parts carry no family title simply has no such column.
|
|
117
|
+
*/
|
|
118
|
+
export const FAMILY_TITLE_COLUMN = 'Family Title';
|
|
119
|
+
/**
|
|
120
|
+
* The CSV column saying how a holder seats in the spindle: `taper` or `face`.
|
|
121
|
+
*
|
|
122
|
+
* `face` is dual contact — the flange face seats at the same time as the cone.
|
|
123
|
+
* REGO-FIX resolves it from its own `form_name` and MariTool from its `Taper`
|
|
124
|
+
* cell, which is the right shape: **how** a vendor states it is that vendor's
|
|
125
|
+
* business, and what the column is called is not.
|
|
126
|
+
*/
|
|
127
|
+
export const CONTACT_COLUMN = 'contact';
|
|
128
|
+
/**
|
|
129
|
+
* The CSV column naming the collet series a holder accepts — `ER16`, `PG25`.
|
|
130
|
+
*
|
|
131
|
+
* The join key between a holder family and a collet family, which is exactly
|
|
132
|
+
* why it cannot be spelled twice: `families/kennametal.ts` states the join
|
|
133
|
+
* against this column, and two spellings of it join to nothing. Both vendors
|
|
134
|
+
* that publish it close the vendor's own spacing before writing it here, for
|
|
135
|
+
* the same reason.
|
|
136
|
+
*/
|
|
137
|
+
export const COLLET_SERIES_COLUMN = 'CST';
|
|
138
|
+
/**
|
|
139
|
+
* The CSV columns carrying a holder's gage length, one per unit system.
|
|
140
|
+
*
|
|
141
|
+
* A **pair** with exactly one cell filled, rather than one column and a unit
|
|
142
|
+
* tag, because a single catalog page can publish both: MariTool gages
|
|
143
|
+
* `HSK40E-ER11-40` in millimetres and `HSK40E-ER16-3.0M` in inches on one
|
|
144
|
+
* listing. Nothing is converted between them — the vendor's own imperial
|
|
145
|
+
* conversion is unusable and computing one here would put a number in the file
|
|
146
|
+
* the vendor never published.
|
|
147
|
+
*
|
|
148
|
+
* REGO-FIX fills only the millimetre cell, because its DIN 4000 documents are
|
|
149
|
+
* metric throughout; the pair is still the shape, so the two vendors' holder
|
|
150
|
+
* CSVs answer the same question with the same columns.
|
|
151
|
+
*/
|
|
152
|
+
export const GAGE_COLUMNS = {
|
|
153
|
+
inches: 'L1_in',
|
|
154
|
+
millimeters: 'L1_mm',
|
|
155
|
+
};
|
|
67
156
|
/**
|
|
68
157
|
* The prefix an unmapped vendor code keeps, so it cannot read as a dimension.
|
|
69
158
|
*
|
|
@@ -86,16 +175,38 @@ export const IDENTITY_COLUMNS = ['Material Number', 'ISO Catalog Number'];
|
|
|
86
175
|
* Where a vendor's CSV does not use {@link IDENTITY_COLUMNS}, and what it uses
|
|
87
176
|
* instead.
|
|
88
177
|
*
|
|
89
|
-
* **
|
|
90
|
-
* adopted Kennametal's identity labels; Destiny Tool passes
|
|
91
|
-
* `itemNumber` straight through and publishes no catalog
|
|
92
|
-
* the convention was real but informal, and it eroded the
|
|
93
|
-
* did not resemble the first two. Writing the deviation
|
|
94
|
-
*
|
|
95
|
-
* happened.
|
|
178
|
+
* **Three entries, and they are a record of drift rather than a licence.**
|
|
179
|
+
* REGO-FIX adopted Kennametal's identity labels; Destiny Tool passes
|
|
180
|
+
* Firestore's own `itemNumber` straight through and publishes no catalog
|
|
181
|
+
* designation at all — the convention was real but informal, and it eroded the
|
|
182
|
+
* first time a vendor did not resemble the first two. Writing the deviation
|
|
183
|
+
* down is what makes the next vendor's drift a decision somebody made rather
|
|
184
|
+
* than a thing that happened.
|
|
185
|
+
*
|
|
186
|
+
* Harvey Tool and MariTool are the honest kind, and they are now the majority:
|
|
187
|
+
* both genuinely publish one identifier per part, so their entries record a
|
|
188
|
+
* fact about the vendor rather than a shortcut taken here. Two of the two
|
|
189
|
+
* vendors added since the convention was written have needed one, which says
|
|
190
|
+
* the two-column shape is Kennametal's rather than the industry's.
|
|
96
191
|
*/
|
|
97
192
|
export const IDENTITY_DEVIATIONS = {
|
|
98
193
|
destinytool: ['itemNumber'],
|
|
194
|
+
// Harvey Tool publishes exactly one number per part — the `Tool #` its own
|
|
195
|
+
// table column is headed with, which is also the segment of its per-part URL
|
|
196
|
+
// — and no second catalog designation anywhere on a product page or a part
|
|
197
|
+
// page. Inventing an `ISO Catalog Number` to satisfy the convention would put
|
|
198
|
+
// a column in the CSV that the vendor does not publish, which is the one
|
|
199
|
+
// thing a receipt must not do.
|
|
200
|
+
harvey: ['Tool #'],
|
|
201
|
+
// MariTool publishes one number per part — the `Part#` line its own listing
|
|
202
|
+
// rows are headed with, which is also what its `Available Downloads for …`
|
|
203
|
+
// header restates and what its search endpoint matches on — and no second
|
|
204
|
+
// catalog designation anywhere on a category page or a product page. The
|
|
205
|
+
// store's `products_id` is not that second number: it is an internal id a
|
|
206
|
+
// re-created product would change, and every guid minted off it with it.
|
|
207
|
+
// Same call as Harvey's, and the honest kind: the entry records a fact about
|
|
208
|
+
// the vendor rather than a shortcut taken here.
|
|
209
|
+
maritool: ['Material Number'],
|
|
99
210
|
};
|
|
100
211
|
/**
|
|
101
212
|
* A vendor's bare column label, suffixed for `unit`.
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* cannot read.** The variants endpoint changed shape, a scrape's row count
|
|
11
11
|
* disagrees with the declared one, a DIN 4000 code that should be pinned is
|
|
12
12
|
* absent. The catalog is fine; the world moved.
|
|
13
|
+
* - {@link IncompletePartError} — **one part is missing a measurement it
|
|
14
|
+
* cannot be a part without.** A narrower case of the second, and the only
|
|
15
|
+
* one `registry.toRecords` may skip past: see below.
|
|
13
16
|
*
|
|
14
17
|
* They throw rather than exiting the process: a Node backend imports this, and
|
|
15
18
|
* a mapped column that moved must not take down somebody's request handler.
|
|
@@ -43,4 +46,31 @@ export declare class ScraperConfigError extends ScraperError {
|
|
|
43
46
|
*/
|
|
44
47
|
export declare class VendorResponseError extends ScraperError {
|
|
45
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* One part does not publish a dimension its kind requires.
|
|
51
|
+
*
|
|
52
|
+
* **The only failure a whole family survives.** `registry.toRecords` maps a
|
|
53
|
+
* family's rows together, so before this type existed every refusal was
|
|
54
|
+
* equally fatal: one part with an unpublished cell ended the conversion and
|
|
55
|
+
* took every other row with it. EMUGE-FRANKEN omits `overall length l₁` on
|
|
56
|
+
* roughly 175 of its 7,021 end mill variants, and both end mill families
|
|
57
|
+
* produced nothing at all because of them.
|
|
58
|
+
*
|
|
59
|
+
* It is a distinct type rather than a flag on {@link VendorResponseError}
|
|
60
|
+
* because the two must not be skipped alike. A cutting material this package
|
|
61
|
+
* has no word for, a point-angle column a family stopped mapping, a variants
|
|
62
|
+
* table that changed shape — those are the vendor's vocabulary or this
|
|
63
|
+
* package's map having moved, and skipping past them quietly is how a scraper
|
|
64
|
+
* starts publishing a catalog nobody checked. Only `columns.required` raises
|
|
65
|
+
* this one, and only for a cell the vendor left unpublished.
|
|
66
|
+
*
|
|
67
|
+
* **It is not a licence to relax a kind's contract.** `records.RECORD_GEOMETRY`
|
|
68
|
+
* still says an end mill always has an `OAL`, and that stays true of every
|
|
69
|
+
* record this package emits: a part without one does not become a record with
|
|
70
|
+
* a hole in it, it becomes no record and a warning. Where a *vendor* genuinely
|
|
71
|
+
* never publishes a field, the answer is still `sometimes` — that is what a
|
|
72
|
+
* drill's `SIG` is, and why one is a contract decision and the other is not.
|
|
73
|
+
*/
|
|
74
|
+
export declare class IncompletePartError extends VendorResponseError {
|
|
75
|
+
}
|
|
46
76
|
export {};
|
package/dist/errors.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* cannot read.** The variants endpoint changed shape, a scrape's row count
|
|
11
11
|
* disagrees with the declared one, a DIN 4000 code that should be pinned is
|
|
12
12
|
* absent. The catalog is fine; the world moved.
|
|
13
|
+
* - {@link IncompletePartError} — **one part is missing a measurement it
|
|
14
|
+
* cannot be a part without.** A narrower case of the second, and the only
|
|
15
|
+
* one `registry.toRecords` may skip past: see below.
|
|
13
16
|
*
|
|
14
17
|
* They throw rather than exiting the process: a Node backend imports this, and
|
|
15
18
|
* a mapped column that moved must not take down somebody's request handler.
|
|
@@ -51,3 +54,30 @@ export class ScraperConfigError extends ScraperError {
|
|
|
51
54
|
*/
|
|
52
55
|
export class VendorResponseError extends ScraperError {
|
|
53
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* One part does not publish a dimension its kind requires.
|
|
59
|
+
*
|
|
60
|
+
* **The only failure a whole family survives.** `registry.toRecords` maps a
|
|
61
|
+
* family's rows together, so before this type existed every refusal was
|
|
62
|
+
* equally fatal: one part with an unpublished cell ended the conversion and
|
|
63
|
+
* took every other row with it. EMUGE-FRANKEN omits `overall length l₁` on
|
|
64
|
+
* roughly 175 of its 7,021 end mill variants, and both end mill families
|
|
65
|
+
* produced nothing at all because of them.
|
|
66
|
+
*
|
|
67
|
+
* It is a distinct type rather than a flag on {@link VendorResponseError}
|
|
68
|
+
* because the two must not be skipped alike. A cutting material this package
|
|
69
|
+
* has no word for, a point-angle column a family stopped mapping, a variants
|
|
70
|
+
* table that changed shape — those are the vendor's vocabulary or this
|
|
71
|
+
* package's map having moved, and skipping past them quietly is how a scraper
|
|
72
|
+
* starts publishing a catalog nobody checked. Only `columns.required` raises
|
|
73
|
+
* this one, and only for a cell the vendor left unpublished.
|
|
74
|
+
*
|
|
75
|
+
* **It is not a licence to relax a kind's contract.** `records.RECORD_GEOMETRY`
|
|
76
|
+
* still says an end mill always has an `OAL`, and that stays true of every
|
|
77
|
+
* record this package emits: a part without one does not become a record with
|
|
78
|
+
* a hole in it, it becomes no record and a warning. Where a *vendor* genuinely
|
|
79
|
+
* never publishes a field, the answer is still `sometimes` — that is what a
|
|
80
|
+
* drill's `SIG` is, and why one is a contract decision and the other is not.
|
|
81
|
+
*/
|
|
82
|
+
export class IncompletePartError extends VendorResponseError {
|
|
83
|
+
}
|