@toolpath/tool-scraper 0.1.0 → 2.0.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 +62 -0
- package/dist/columns.js +62 -0
- package/dist/conventions.d.ts +89 -15
- package/dist/conventions.js +108 -15
- package/dist/families/emuge.d.ts +183 -0
- package/dist/families/emuge.js +159 -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 +152 -2
- package/dist/node/main.js +0 -0
- package/dist/records.d.ts +169 -12
- package/dist/records.js +116 -5
- package/dist/registry.d.ts +31 -1
- package/dist/registry.js +40 -1
- 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 +41 -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 +150 -0
- package/dist/vendors/emuge/records.js +375 -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/records.d.ts +13 -4
- package/dist/vendors/kennametal/records.js +61 -26
- 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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a machinist's number, and the one constant between the two systems.
|
|
3
|
+
*
|
|
4
|
+
* Three vendors publish dimensions the way a printed catalog does — `.250`,
|
|
5
|
+
* `3/4`, `1-1/2` — and until this module each adapter had its own reader for
|
|
6
|
+
* that grammar. They disagreed on real inputs: REGO-FIX's `parseSize` handled
|
|
7
|
+
* no mixed number at all, so `1-1/2` reached `Number()` as `NaN` and was
|
|
8
|
+
* refused; Destiny Tool's `parseFractionInches` branched on a `.` first, so
|
|
9
|
+
* `1.5-1/2` was refused there and read as 2 by Harvey's. And they disagreed on
|
|
10
|
+
* *how* they refused — two threw `RangeError`, one answered `null`.
|
|
11
|
+
*
|
|
12
|
+
* That is the same shape `fetch.createFetcher` and `scrape.pause` were pulled
|
|
13
|
+
* out for, and the same argument: four copies of four lines is not expensive,
|
|
14
|
+
* four copies of the **decisions** in them is, because the decisions are the
|
|
15
|
+
* part that has to be consistent. A dimension string is the last thing in this
|
|
16
|
+
* package that should mean two things.
|
|
17
|
+
*
|
|
18
|
+
* ## The reader answers `null`; the caller decides what that costs
|
|
19
|
+
*
|
|
20
|
+
* {@link fractionValue} is the primitive, and it never throws: whether an
|
|
21
|
+
* unreadable cell is a skipped row, a warning or a refusal is a vendor's call,
|
|
22
|
+
* and every adapter here already makes it differently for good reasons. Harvey
|
|
23
|
+
* skips a `-` cell because the column does not apply to that row; REGO-FIX
|
|
24
|
+
* refuses, because a collet with no size is not a part. Pushing that decision
|
|
25
|
+
* into the reader would take it away from the module that knows.
|
|
26
|
+
*
|
|
27
|
+
* ## The two decisions about a *read* cell live here too
|
|
28
|
+
*
|
|
29
|
+
* {@link fractionValue} answers what a token says. {@link asLength} and
|
|
30
|
+
* {@link asCount} answer what that costs, and they are core for the same reason
|
|
31
|
+
* the grammar is: a cell stating `mm` in a family published in inches is
|
|
32
|
+
* converted-and-warned, and a cell stating an angle in a length column is
|
|
33
|
+
* refused, and those two calls have to be the same call whoever is being
|
|
34
|
+
* scraped. They were a verbatim copy in `vendors/harvey/value.ts` and
|
|
35
|
+
* `vendors/emuge/value.ts` — down to the wording of both warnings — which is
|
|
36
|
+
* two operators reading the same sentence out of two files that were free to
|
|
37
|
+
* drift apart.
|
|
38
|
+
*
|
|
39
|
+
* A vendor's own reader still owns the **grammar**, because that genuinely
|
|
40
|
+
* differs: Harvey writes a mixed number `1-1/2` and annotates it `(1/8)`, EMUGE
|
|
41
|
+
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
|
+
* {@link Measured}, which is the part they have in common.
|
|
43
|
+
*
|
|
44
|
+
* **`MM_PER_INCH` lives here and not in a vendor.** It was declared and
|
|
45
|
+
* exported twice — `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts`
|
|
46
|
+
* — so `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
47
|
+
* published their own copy of 25.4. That is exactly what `conventions.CAD_COLUMN`
|
|
48
|
+
* was moved up for, and what `tests/vendor-boundary.test.ts` now refuses by name.
|
|
49
|
+
*/
|
|
50
|
+
import type { UnitSystem } from './conventions.js';
|
|
51
|
+
import { type Warn } from './scrape.js';
|
|
52
|
+
/** Exact by definition: the inch has been 25.4 mm since 1959. */
|
|
53
|
+
export declare const MM_PER_INCH = 25.4;
|
|
54
|
+
/**
|
|
55
|
+
* `.250` -> 0.25, `3/4` -> 0.75, `1-1/2` -> 1.5, `2` -> 2.
|
|
56
|
+
*
|
|
57
|
+
* Null where the token is not one of those four shapes — including a division
|
|
58
|
+
* by zero, which arrives finite-looking as `Infinity` and would otherwise
|
|
59
|
+
* travel into a row as a dimension.
|
|
60
|
+
*
|
|
61
|
+
* Unit-free on purpose: what system the number is in is the caller's, from the
|
|
62
|
+
* family's declared `unit` or from a unit the cell states outright. A reader
|
|
63
|
+
* that guessed would be the mistake `conventions.UNIT_SUFFIX` exists to prevent.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fractionValue(token: string): number | null;
|
|
66
|
+
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
67
|
+
export declare function convertLength(value: number, from: UnitSystem, to: UnitSystem): number;
|
|
68
|
+
/**
|
|
69
|
+
* What a value states about itself: a length system, or degrees.
|
|
70
|
+
*
|
|
71
|
+
* Degrees are inside this union rather than beside it because a cell states one
|
|
72
|
+
* thing about itself — "this is an angle" competes with "this is millimetres"
|
|
73
|
+
* and is never both. `vendors/harvey/value.ts` carries the same fact as a
|
|
74
|
+
* separate `degrees` boolean, which is that vendor's published shape and stays.
|
|
75
|
+
*/
|
|
76
|
+
export type StatedUnit = UnitSystem | 'degrees';
|
|
77
|
+
/**
|
|
78
|
+
* One cell as a vendor's own reader left it.
|
|
79
|
+
*
|
|
80
|
+
* The part every vendor's reader has in common, and all {@link asLength} and
|
|
81
|
+
* {@link asCount} need. A reader is free to answer more — Harvey's also carries
|
|
82
|
+
* the parenthesised annotation, the `(1.5x)` ratio and the non-numeric code —
|
|
83
|
+
* and anything with these two fields is accepted here.
|
|
84
|
+
*/
|
|
85
|
+
export interface Measured {
|
|
86
|
+
/** The number the cell states, in {@link Measured.stated}. Null where none. */
|
|
87
|
+
readonly value: number | null;
|
|
88
|
+
/** The unit the cell names outright. Null where it names none. */
|
|
89
|
+
readonly stated: StatedUnit | null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A read cell as a length in `unit`, or null where it publishes none.
|
|
93
|
+
*
|
|
94
|
+
* A cell that states its own unit and disagrees with the family's is
|
|
95
|
+
* **converted and warned about** rather than refused: the value is right, the
|
|
96
|
+
* column's suffix is right, and the parts this happens on are real tools
|
|
97
|
+
* somebody can order. Refusing would drop them; trusting the column's unit
|
|
98
|
+
* would publish a 3-inch shank.
|
|
99
|
+
*
|
|
100
|
+
* An angle reaching a dimensional column is **refused**, because that is a
|
|
101
|
+
* header that has moved rather than a value that is odd.
|
|
102
|
+
*
|
|
103
|
+
* `display` is passed beside `parsed` only so the warnings can quote the cell
|
|
104
|
+
* the vendor really wrote, which is the part an operator needs to go look at.
|
|
105
|
+
*/
|
|
106
|
+
export declare function asLength(parsed: Measured, display: string, unit: UnitSystem, what: string, warn?: Warn): number | null;
|
|
107
|
+
/**
|
|
108
|
+
* A read cell as a whole count — a flute or tooth number. Null where there is
|
|
109
|
+
* none, and null for a fraction, which is a column that has moved rather than a
|
|
110
|
+
* tool with two and a half flutes.
|
|
111
|
+
*/
|
|
112
|
+
export declare function asCount(parsed: Measured): number | null;
|
package/dist/measure.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a machinist's number, and the one constant between the two systems.
|
|
3
|
+
*
|
|
4
|
+
* Three vendors publish dimensions the way a printed catalog does — `.250`,
|
|
5
|
+
* `3/4`, `1-1/2` — and until this module each adapter had its own reader for
|
|
6
|
+
* that grammar. They disagreed on real inputs: REGO-FIX's `parseSize` handled
|
|
7
|
+
* no mixed number at all, so `1-1/2` reached `Number()` as `NaN` and was
|
|
8
|
+
* refused; Destiny Tool's `parseFractionInches` branched on a `.` first, so
|
|
9
|
+
* `1.5-1/2` was refused there and read as 2 by Harvey's. And they disagreed on
|
|
10
|
+
* *how* they refused — two threw `RangeError`, one answered `null`.
|
|
11
|
+
*
|
|
12
|
+
* That is the same shape `fetch.createFetcher` and `scrape.pause` were pulled
|
|
13
|
+
* out for, and the same argument: four copies of four lines is not expensive,
|
|
14
|
+
* four copies of the **decisions** in them is, because the decisions are the
|
|
15
|
+
* part that has to be consistent. A dimension string is the last thing in this
|
|
16
|
+
* package that should mean two things.
|
|
17
|
+
*
|
|
18
|
+
* ## The reader answers `null`; the caller decides what that costs
|
|
19
|
+
*
|
|
20
|
+
* {@link fractionValue} is the primitive, and it never throws: whether an
|
|
21
|
+
* unreadable cell is a skipped row, a warning or a refusal is a vendor's call,
|
|
22
|
+
* and every adapter here already makes it differently for good reasons. Harvey
|
|
23
|
+
* skips a `-` cell because the column does not apply to that row; REGO-FIX
|
|
24
|
+
* refuses, because a collet with no size is not a part. Pushing that decision
|
|
25
|
+
* into the reader would take it away from the module that knows.
|
|
26
|
+
*
|
|
27
|
+
* ## The two decisions about a *read* cell live here too
|
|
28
|
+
*
|
|
29
|
+
* {@link fractionValue} answers what a token says. {@link asLength} and
|
|
30
|
+
* {@link asCount} answer what that costs, and they are core for the same reason
|
|
31
|
+
* the grammar is: a cell stating `mm` in a family published in inches is
|
|
32
|
+
* converted-and-warned, and a cell stating an angle in a length column is
|
|
33
|
+
* refused, and those two calls have to be the same call whoever is being
|
|
34
|
+
* scraped. They were a verbatim copy in `vendors/harvey/value.ts` and
|
|
35
|
+
* `vendors/emuge/value.ts` — down to the wording of both warnings — which is
|
|
36
|
+
* two operators reading the same sentence out of two files that were free to
|
|
37
|
+
* drift apart.
|
|
38
|
+
*
|
|
39
|
+
* A vendor's own reader still owns the **grammar**, because that genuinely
|
|
40
|
+
* differs: Harvey writes a mixed number `1-1/2` and annotates it `(1/8)`, EMUGE
|
|
41
|
+
* writes `1 1/2` and admits no hyphen at all. What it hands here is
|
|
42
|
+
* {@link Measured}, which is the part they have in common.
|
|
43
|
+
*
|
|
44
|
+
* **`MM_PER_INCH` lives here and not in a vendor.** It was declared and
|
|
45
|
+
* exported twice — `vendors/harvey/value.ts` and `vendors/regofix/scrape.ts`
|
|
46
|
+
* — so `@toolpath/tool-scraper/vendors/harvey` and `.../vendors/regofix` each
|
|
47
|
+
* published their own copy of 25.4. That is exactly what `conventions.CAD_COLUMN`
|
|
48
|
+
* was moved up for, and what `tests/vendor-boundary.test.ts` now refuses by name.
|
|
49
|
+
*/
|
|
50
|
+
import { consoleWarn } from './scrape.js';
|
|
51
|
+
/** Exact by definition: the inch has been 25.4 mm since 1959. */
|
|
52
|
+
export const MM_PER_INCH = 25.4;
|
|
53
|
+
/**
|
|
54
|
+
* A decimal, a simple fraction or a mixed number — and nothing else.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately strict, because it is used without a pre-guard. The whole-number
|
|
57
|
+
* part of a mixed number is `\d+` rather than a decimal so that a **range** like
|
|
58
|
+
* `.035-.040` — which Destiny Tool really does publish, in description text —
|
|
59
|
+
* cannot parse as `0.035 + 0.040`. A range is not a measurement, and reading one
|
|
60
|
+
* as a sum is the kind of quiet wrong number this package exists to avoid.
|
|
61
|
+
*/
|
|
62
|
+
const NUMERIC = /^(?:(\d+)-)?(\d*\.?\d+)(?:\/(\d*\.?\d+))?$/;
|
|
63
|
+
/**
|
|
64
|
+
* `.250` -> 0.25, `3/4` -> 0.75, `1-1/2` -> 1.5, `2` -> 2.
|
|
65
|
+
*
|
|
66
|
+
* Null where the token is not one of those four shapes — including a division
|
|
67
|
+
* by zero, which arrives finite-looking as `Infinity` and would otherwise
|
|
68
|
+
* travel into a row as a dimension.
|
|
69
|
+
*
|
|
70
|
+
* Unit-free on purpose: what system the number is in is the caller's, from the
|
|
71
|
+
* family's declared `unit` or from a unit the cell states outright. A reader
|
|
72
|
+
* that guessed would be the mistake `conventions.UNIT_SUFFIX` exists to prevent.
|
|
73
|
+
*/
|
|
74
|
+
export function fractionValue(token) {
|
|
75
|
+
const parsed = NUMERIC.exec(token.trim());
|
|
76
|
+
if (parsed === null)
|
|
77
|
+
return null;
|
|
78
|
+
const [, whole, numerator, denominator] = parsed;
|
|
79
|
+
const part = denominator === undefined ? Number(numerator) : Number(numerator) / Number(denominator);
|
|
80
|
+
const value = (whole === undefined ? 0 : Number(whole)) + part;
|
|
81
|
+
return Number.isFinite(value) ? value : null;
|
|
82
|
+
}
|
|
83
|
+
/** `value`, converted from `from` to `to`. A no-op when they agree. */
|
|
84
|
+
export function convertLength(value, from, to) {
|
|
85
|
+
if (from === to)
|
|
86
|
+
return value;
|
|
87
|
+
return to === 'inches' ? value / MM_PER_INCH : value * MM_PER_INCH;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A read cell as a length in `unit`, or null where it publishes none.
|
|
91
|
+
*
|
|
92
|
+
* A cell that states its own unit and disagrees with the family's is
|
|
93
|
+
* **converted and warned about** rather than refused: the value is right, the
|
|
94
|
+
* column's suffix is right, and the parts this happens on are real tools
|
|
95
|
+
* somebody can order. Refusing would drop them; trusting the column's unit
|
|
96
|
+
* would publish a 3-inch shank.
|
|
97
|
+
*
|
|
98
|
+
* An angle reaching a dimensional column is **refused**, because that is a
|
|
99
|
+
* header that has moved rather than a value that is odd.
|
|
100
|
+
*
|
|
101
|
+
* `display` is passed beside `parsed` only so the warnings can quote the cell
|
|
102
|
+
* the vendor really wrote, which is the part an operator needs to go look at.
|
|
103
|
+
*/
|
|
104
|
+
export function asLength(parsed, display, unit, what, warn = consoleWarn) {
|
|
105
|
+
const { value, stated } = parsed;
|
|
106
|
+
if (value === null)
|
|
107
|
+
return null;
|
|
108
|
+
if (stated === 'degrees') {
|
|
109
|
+
warn(` WARNING: ${what}: ${JSON.stringify(display)} is an angle in a column ` +
|
|
110
|
+
`read as a length — skipped`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (stated !== null && stated !== unit) {
|
|
114
|
+
warn(` WARNING: ${what}: ${JSON.stringify(display)} states ${stated} in a ` +
|
|
115
|
+
`family published in ${unit} — converted`);
|
|
116
|
+
return convertLength(value, stated, unit);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A read cell as a whole count — a flute or tooth number. Null where there is
|
|
122
|
+
* none, and null for a fraction, which is a column that has moved rather than a
|
|
123
|
+
* tool with two and a half flutes.
|
|
124
|
+
*/
|
|
125
|
+
export function asCount(parsed) {
|
|
126
|
+
const { value } = parsed;
|
|
127
|
+
if (value === null || !Number.isInteger(value))
|
|
128
|
+
return null;
|
|
129
|
+
return value;
|
|
130
|
+
}
|
package/dist/node/cli.d.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* toolpath-scrape kennametal family page -> CSV
|
|
6
6
|
* toolpath-scrape regofix ProductFinder index -> toolholding CSV
|
|
7
7
|
* toolpath-scrape destinytool Firestore products -> End Mill CSV
|
|
8
|
+
* toolpath-scrape harvey one product page -> CSV
|
|
9
|
+
* toolpath-scrape maritool leaf categories -> toolholding CSV
|
|
10
|
+
* toolpath-scrape emuge one catalog category -> CSV
|
|
8
11
|
* toolpath-scrape thread-pitch add the derived Thread Pitch column
|
|
9
12
|
* toolpath-scrape cad add the vendor CAD model column
|
|
10
13
|
* toolpath-scrape materials add the ISO workpiece-group column
|
package/dist/node/cli.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* toolpath-scrape kennametal family page -> CSV
|
|
6
6
|
* toolpath-scrape regofix ProductFinder index -> toolholding CSV
|
|
7
7
|
* toolpath-scrape destinytool Firestore products -> End Mill CSV
|
|
8
|
+
* toolpath-scrape harvey one product page -> CSV
|
|
9
|
+
* toolpath-scrape maritool leaf categories -> toolholding CSV
|
|
10
|
+
* toolpath-scrape emuge one catalog category -> CSV
|
|
8
11
|
* toolpath-scrape thread-pitch add the derived Thread Pitch column
|
|
9
12
|
* toolpath-scrape cad add the vendor CAD model column
|
|
10
13
|
* toolpath-scrape materials add the ISO workpiece-group column
|
|
@@ -23,14 +26,15 @@
|
|
|
23
26
|
* or an assembly catalog is a different product, and none of it is in this
|
|
24
27
|
* tree.
|
|
25
28
|
*/
|
|
26
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
-
import { basename } from 'node:path';
|
|
29
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { basename, dirname } from 'node:path';
|
|
28
31
|
import { ScraperConfigError, VendorResponseError } from '../errors.js';
|
|
29
32
|
import { familyBrand } from '../family.js';
|
|
30
33
|
import { ALL_FAMILIES, FAMILIES, HOLDER_FAMILIES, familyConfig } from '../families/index.js';
|
|
31
34
|
import { createFetcher } from '../fetch.js';
|
|
32
35
|
import { AEM_BRANDS } from '../identity.js';
|
|
33
36
|
import { boundFamily } from '../registry.js';
|
|
37
|
+
import { pause, REQUEST_DELAY_MS } from '../scrape.js';
|
|
34
38
|
import { mirrorFamilySteps } from './cad-mirror.js';
|
|
35
39
|
import { parseCsv, toCsv } from './csv.js';
|
|
36
40
|
import { describeRoot, familyCsv, stepDir } from './paths.js';
|
|
@@ -40,7 +44,15 @@ import { annotateCadUrls } from '../vendors/kennametal/cad.js';
|
|
|
40
44
|
import { addMaterialGroups, groupsByMaterial } from '../vendors/kennametal/materials.js';
|
|
41
45
|
import { addThreadPitch } from '../vendors/kennametal/thread-column.js';
|
|
42
46
|
import { scrapeEndMills, DOCUMENTS_URL } from '../vendors/destinytool/scrape.js';
|
|
47
|
+
import { PRODUCT_PAGES } from '../families/harvey.js';
|
|
48
|
+
import { CATEGORY_ROOTS, discoverProducts } from '../vendors/harvey/catalog.js';
|
|
49
|
+
import { scrapeProduct } from '../vendors/harvey/scrape.js';
|
|
43
50
|
import { SEARCH_URL, scrapeCollets, scrapeHolders } from '../vendors/regofix/scrape.js';
|
|
51
|
+
import { LEAVES as MARITOOL_LEAVES } from '../families/maritool.js';
|
|
52
|
+
import { CATEGORY_ROOTS as MARITOOL_ROOTS, describe as describeCategory, discoverCategories, leavesOf, } from '../vendors/maritool/catalog.js';
|
|
53
|
+
import { scrapeHolders as scrapeMaritoolHolders } from '../vendors/maritool/scrape.js';
|
|
54
|
+
import { SCRAPE_TARGETS as EMUGE_TARGETS } from '../families/emuge.js';
|
|
55
|
+
import { scrapeCategory } from '../vendors/emuge/scrape.js';
|
|
44
56
|
/**
|
|
45
57
|
* The PG series a BT 30 holder can take. PG 32 and PG 48 collets exist and no
|
|
46
58
|
* BT 30 holder in this catalog accepts one, so scraping them would add parts
|
|
@@ -67,6 +79,35 @@ const USAGE = `usage: toolpath-scrape <command> [args]
|
|
|
67
79
|
Pages the whole Destiny Tool \`products\` Firestore collection and writes
|
|
68
80
|
every End Mill row.
|
|
69
81
|
|
|
82
|
+
harvey FAMILY.csv [more.csv ...]
|
|
83
|
+
One Harvey Tool product page -> a CSV. The page to fetch and the unit
|
|
84
|
+
system come from the family's own config, so neither is typed again.
|
|
85
|
+
One HTML row is up to nine orderable parts; expect more rows out than
|
|
86
|
+
the vendor's table appears to have.
|
|
87
|
+
|
|
88
|
+
harvey --catalog
|
|
89
|
+
Walks the four Harvey category trees and prints every product page it
|
|
90
|
+
finds, one per line. For noticing a page Harvey has added — a scrape
|
|
91
|
+
needs none of it.
|
|
92
|
+
|
|
93
|
+
maritool FAMILY.csv [more.csv ...]
|
|
94
|
+
One MariTool taper -> a CSV. The leaf categories to page through come
|
|
95
|
+
from the family's own config, so none of them is typed again. One
|
|
96
|
+
request per listing page, then one per part for its Product
|
|
97
|
+
Specifications table.
|
|
98
|
+
|
|
99
|
+
maritool --catalog
|
|
100
|
+
Walks the five MariTool taper trees and prints every category it finds,
|
|
101
|
+
one per line, with its product count. For rechecking the leaf cPaths in
|
|
102
|
+
\`families/maritool.ts\` — a scrape needs none of it.
|
|
103
|
+
|
|
104
|
+
emuge FAMILY.csv [more.csv ...]
|
|
105
|
+
One EMUGE-FRANKEN catalog category -> a CSV. The category, the facet
|
|
106
|
+
narrowing it and the unit system all come from the family's own config,
|
|
107
|
+
so none of them is typed again. One request per page of grouped
|
|
108
|
+
products, one per group for its orderable parts, and one per 30 parts
|
|
109
|
+
for the fields only a per-part record carries.
|
|
110
|
+
|
|
70
111
|
thread-pitch TAP.csv [more.csv ...]
|
|
71
112
|
Adds a Thread Pitch column derived from D1-TDZ, in place. Safe to re-run.
|
|
72
113
|
|
|
@@ -158,6 +199,12 @@ export async function run(argv, io = STDOUT, fetcher = createFetcher()) {
|
|
|
158
199
|
return regofix(rest, io, fetcher);
|
|
159
200
|
case 'destinytool':
|
|
160
201
|
return destinytool(rest, io, fetcher);
|
|
202
|
+
case 'harvey':
|
|
203
|
+
return harvey(rest, io, fetcher);
|
|
204
|
+
case 'maritool':
|
|
205
|
+
return maritool(rest, io, fetcher);
|
|
206
|
+
case 'emuge':
|
|
207
|
+
return emuge(rest, io, fetcher);
|
|
161
208
|
case 'thread-pitch':
|
|
162
209
|
return threadPitch(rest, io);
|
|
163
210
|
case 'cad':
|
|
@@ -251,6 +298,109 @@ async function destinytool(argv, io, fetcher) {
|
|
|
251
298
|
wrote(argv[0], 'destinytool', { ...scrape, source: DOCUMENTS_URL }, io);
|
|
252
299
|
return 0;
|
|
253
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* One or more Harvey families, or the catalog walk.
|
|
303
|
+
*
|
|
304
|
+
* Paced between pages by the package's shared politeness delay, which the
|
|
305
|
+
* per-page scrape does not do for itself: one family is one request, and a
|
|
306
|
+
* caller scraping one has nothing to wait for.
|
|
307
|
+
*/
|
|
308
|
+
async function harvey(argv, io, fetcher) {
|
|
309
|
+
if (argv[0] === '--catalog') {
|
|
310
|
+
const found = await discoverProducts(fetcher, CATEGORY_ROOTS, { warn: io.error });
|
|
311
|
+
for (const path of found)
|
|
312
|
+
io.log(path);
|
|
313
|
+
io.log(`${found.length} product pages under ${CATEGORY_ROOTS.length} category trees`);
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
if (argv.length === 0) {
|
|
317
|
+
io.error(USAGE);
|
|
318
|
+
return 2;
|
|
319
|
+
}
|
|
320
|
+
const names = namesIn(argv, PRODUCT_PAGES, 'Harvey family');
|
|
321
|
+
for (const [index, name] of names.entries()) {
|
|
322
|
+
if (index > 0)
|
|
323
|
+
await pause(REQUEST_DELAY_MS);
|
|
324
|
+
const cfg = boundFamily(name);
|
|
325
|
+
// Never undefined: `namesIn` refused anything `PRODUCT_PAGES` does not key,
|
|
326
|
+
// and `tests/harvey-families.test.ts` holds the two tables to the same keys.
|
|
327
|
+
const page = PRODUCT_PAGES[name];
|
|
328
|
+
if (cfg.unit === undefined) {
|
|
329
|
+
throw new ScraperConfigError(name, 'declares no unit — every Harvey family publishes one');
|
|
330
|
+
}
|
|
331
|
+
const scrape = await scrapeProduct(fetcher, page, { unit: cfg.unit, warn: io.error });
|
|
332
|
+
const out = familyCsv(name);
|
|
333
|
+
// This command resolves its own output path rather than taking one, so the
|
|
334
|
+
// brand's directory may not exist yet — unlike `kennametal`, where the path
|
|
335
|
+
// is typed and its directory is the caller's to have made.
|
|
336
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
337
|
+
wrote(out, 'harvey', scrape, io);
|
|
338
|
+
}
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* One or more MariTool families, or the catalog walk.
|
|
343
|
+
*
|
|
344
|
+
* The scrape paces itself between every request it makes, so unlike `harvey`
|
|
345
|
+
* there is nothing to pace between families here.
|
|
346
|
+
*/
|
|
347
|
+
async function maritool(argv, io, fetcher) {
|
|
348
|
+
if (argv[0] === '--catalog') {
|
|
349
|
+
const found = await discoverCategories(fetcher, MARITOOL_ROOTS, { warn: io.error });
|
|
350
|
+
for (const category of found)
|
|
351
|
+
io.log(describeCategory(category));
|
|
352
|
+
const leaves = leavesOf(found);
|
|
353
|
+
io.log(`${found.length} categories under ${MARITOOL_ROOTS.length} taper trees, ` +
|
|
354
|
+
`${leaves.length} of them leaves, ` +
|
|
355
|
+
`${leaves.reduce((sum, leaf) => sum + leaf.products, 0)} products`);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
if (argv.length === 0) {
|
|
359
|
+
io.error(USAGE);
|
|
360
|
+
return 2;
|
|
361
|
+
}
|
|
362
|
+
for (const name of namesIn(argv, MARITOOL_LEAVES, 'MariTool family')) {
|
|
363
|
+
// Never undefined: `namesIn` refused anything `MARITOOL_LEAVES` does not
|
|
364
|
+
// key, and `tests/maritool.test.ts` holds the two tables to the same keys.
|
|
365
|
+
const leaves = MARITOOL_LEAVES[name];
|
|
366
|
+
const scrape = await scrapeMaritoolHolders(fetcher, leaves, { warn: io.error });
|
|
367
|
+
const out = familyCsv(name);
|
|
368
|
+
// This command resolves its own output path rather than taking one, so the
|
|
369
|
+
// brand's directory may not exist yet.
|
|
370
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
371
|
+
wrote(out, 'maritool', scrape, io);
|
|
372
|
+
}
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* One or more EMUGE-FRANKEN families.
|
|
377
|
+
*
|
|
378
|
+
* The scrape paces itself between every request it makes, so — as with
|
|
379
|
+
* `maritool` and unlike `harvey` — there is nothing to pace between families
|
|
380
|
+
* here.
|
|
381
|
+
*/
|
|
382
|
+
async function emuge(argv, io, fetcher) {
|
|
383
|
+
if (argv.length === 0) {
|
|
384
|
+
io.error(USAGE);
|
|
385
|
+
return 2;
|
|
386
|
+
}
|
|
387
|
+
for (const name of namesIn(argv, EMUGE_TARGETS, 'EMUGE-FRANKEN family')) {
|
|
388
|
+
const cfg = boundFamily(name);
|
|
389
|
+
// Never undefined: `namesIn` refused anything `EMUGE_TARGETS` does not key,
|
|
390
|
+
// and `tests/emuge-families.test.ts` holds the two tables to the same keys.
|
|
391
|
+
const target = EMUGE_TARGETS[name];
|
|
392
|
+
if (cfg.unit === undefined) {
|
|
393
|
+
throw new ScraperConfigError(name, 'declares no unit — every EMUGE-FRANKEN family publishes one');
|
|
394
|
+
}
|
|
395
|
+
const scrape = await scrapeCategory(fetcher, target, { unit: cfg.unit, warn: io.error });
|
|
396
|
+
const out = familyCsv(name);
|
|
397
|
+
// This command resolves its own output path rather than taking one, so the
|
|
398
|
+
// brand's directory may not exist yet.
|
|
399
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
400
|
+
wrote(out, 'emuge', scrape, io);
|
|
401
|
+
}
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
254
404
|
function threadPitch(argv, io) {
|
|
255
405
|
if (argv.length === 0) {
|
|
256
406
|
io.error(USAGE);
|
package/dist/node/main.js
CHANGED
|
File without changes
|