orizu 0.0.9 → 0.1.1
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/dist/credentials.js +10 -2
- package/dist/error-response.js +20 -0
- package/dist/index.js +360 -37
- package/dist/jsonl-stream.js +91 -0
- package/dist/task-create-error.js +60 -0
- package/package.json +1 -1
- package/src/credentials.ts +11 -2
- package/src/error-response.ts +27 -0
- package/src/index.ts +499 -51
- package/src/jsonl-stream.ts +131 -0
- package/src/task-create-error.ts +115 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createReadStream } from 'fs'
|
|
2
|
+
import { createInterface } from 'readline'
|
|
3
|
+
|
|
4
|
+
export const JSONL_UPLOAD_CHUNK_MAX_BYTES = 2 * 1024 * 1024
|
|
5
|
+
|
|
6
|
+
interface JsonlChunkOptions {
|
|
7
|
+
maxPayloadBytes?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PendingRow {
|
|
11
|
+
row: Record<string, unknown>
|
|
12
|
+
jsonByteLength: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isObjectRow(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rowsPayloadByteLength(rowCount: number, rowsByteLength: number): number {
|
|
20
|
+
return Buffer.byteLength('{"rows":[]}', 'utf8') +
|
|
21
|
+
rowsByteLength +
|
|
22
|
+
Math.max(0, rowCount - 1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatMb(byteLength: number): string {
|
|
26
|
+
return (byteLength / (1024 * 1024)).toFixed(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function mapReadError(filePath: string, error: unknown): Error {
|
|
30
|
+
const maybeError = error as NodeJS.ErrnoException
|
|
31
|
+
|
|
32
|
+
if (!maybeError.code && error instanceof Error) {
|
|
33
|
+
return error
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (maybeError.code === 'ENOENT') {
|
|
37
|
+
return new Error(
|
|
38
|
+
`File not found: ${filePath}. Check the path and filename, then retry.`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
|
|
43
|
+
return new Error(
|
|
44
|
+
`Cannot read file: ${filePath}. macOS may be blocking access to this folder (for example Downloads). Grant folder permission to your terminal app and retry.`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new Error(`Failed to read file ${filePath}: ${maybeError.message}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function estimateRowsPayloadByteLength(rows: Array<Record<string, unknown>>): number {
|
|
52
|
+
const rowsByteLength = rows.reduce((total, row) => {
|
|
53
|
+
return total + Buffer.byteLength(JSON.stringify(row), 'utf8')
|
|
54
|
+
}, 0)
|
|
55
|
+
|
|
56
|
+
return rowsPayloadByteLength(rows.length, rowsByteLength)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function* streamJsonlRowChunks(
|
|
60
|
+
filePath: string,
|
|
61
|
+
options: JsonlChunkOptions = {}
|
|
62
|
+
): AsyncGenerator<Array<Record<string, unknown>>> {
|
|
63
|
+
const maxPayloadBytes = options.maxPayloadBytes ?? JSONL_UPLOAD_CHUNK_MAX_BYTES
|
|
64
|
+
let chunk: PendingRow[] = []
|
|
65
|
+
let chunkRowsByteLength = 0
|
|
66
|
+
let parsedRowCount = 0
|
|
67
|
+
let lineNumber = 0
|
|
68
|
+
|
|
69
|
+
const stream = createReadStream(filePath, { encoding: 'utf8' })
|
|
70
|
+
const lines = createInterface({
|
|
71
|
+
input: stream,
|
|
72
|
+
crlfDelay: Infinity,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
for await (const line of lines) {
|
|
77
|
+
lineNumber += 1
|
|
78
|
+
const trimmed = line.trim()
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let parsed: unknown
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(trimmed)
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(`Invalid JSONL at line ${lineNumber}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!isObjectRow(parsed)) {
|
|
91
|
+
throw new Error(`Row ${lineNumber} must be a JSON object`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const jsonByteLength = Buffer.byteLength(JSON.stringify(parsed), 'utf8')
|
|
95
|
+
const singleRowPayloadBytes = rowsPayloadByteLength(1, jsonByteLength)
|
|
96
|
+
if (singleRowPayloadBytes > maxPayloadBytes) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Row ${lineNumber} is ${formatMb(singleRowPayloadBytes)} MB after JSON encoding, which exceeds the ${formatMb(maxPayloadBytes)} MB upload chunk limit`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nextPayloadBytes = rowsPayloadByteLength(
|
|
103
|
+
chunk.length + 1,
|
|
104
|
+
chunkRowsByteLength + jsonByteLength
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (chunk.length > 0 && nextPayloadBytes > maxPayloadBytes) {
|
|
108
|
+
yield chunk.map(item => item.row)
|
|
109
|
+
chunk = []
|
|
110
|
+
chunkRowsByteLength = 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
chunk.push({ row: parsed, jsonByteLength })
|
|
114
|
+
chunkRowsByteLength += jsonByteLength
|
|
115
|
+
parsedRowCount += 1
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw mapReadError(filePath, error)
|
|
119
|
+
} finally {
|
|
120
|
+
lines.close()
|
|
121
|
+
stream.destroy()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (chunk.length > 0) {
|
|
125
|
+
yield chunk.map(item => item.row)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (parsedRowCount === 0) {
|
|
129
|
+
throw new Error('Dataset file contains no rows')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export interface TaskCreateErrorPayload {
|
|
2
|
+
error?: string
|
|
3
|
+
invalidAssigneeErrors?: Array<{
|
|
4
|
+
assignee?: string
|
|
5
|
+
reason?: string
|
|
6
|
+
}>
|
|
7
|
+
missingRequiredFields?: string[]
|
|
8
|
+
incompatibleRowCount?: number
|
|
9
|
+
missingRequiredFieldCounts?: Record<string, number>
|
|
10
|
+
versionId?: string
|
|
11
|
+
versionNum?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CliErrorWithStatus extends Error {
|
|
15
|
+
httpStatus?: number
|
|
16
|
+
structuredPayload?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createCliError(
|
|
20
|
+
message: string,
|
|
21
|
+
httpStatus?: number,
|
|
22
|
+
structuredPayload?: Record<string, unknown>
|
|
23
|
+
): CliErrorWithStatus {
|
|
24
|
+
const error = new Error(message) as CliErrorWithStatus
|
|
25
|
+
error.httpStatus = httpStatus
|
|
26
|
+
error.structuredPayload = structuredPayload
|
|
27
|
+
return error
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function formatTaskCreateError(response: Response): Promise<CliErrorWithStatus> {
|
|
31
|
+
const contentType = response.headers.get('content-type') || ''
|
|
32
|
+
const rawBody = await response.text()
|
|
33
|
+
const httpStatus = response.status
|
|
34
|
+
let parsedPayload: Record<string, unknown> | undefined
|
|
35
|
+
|
|
36
|
+
if (!contentType.includes('application/json')) {
|
|
37
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const payload = JSON.parse(rawBody) as TaskCreateErrorPayload
|
|
42
|
+
parsedPayload = payload as unknown as Record<string, unknown>
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
Array.isArray(payload.invalidAssigneeErrors) &&
|
|
46
|
+
payload.invalidAssigneeErrors.length > 0
|
|
47
|
+
) {
|
|
48
|
+
const details = payload.invalidAssigneeErrors
|
|
49
|
+
.filter(
|
|
50
|
+
item =>
|
|
51
|
+
typeof item.assignee === 'string' &&
|
|
52
|
+
item.assignee.length > 0 &&
|
|
53
|
+
typeof item.reason === 'string' &&
|
|
54
|
+
item.reason.length > 0
|
|
55
|
+
)
|
|
56
|
+
.map(item => ` - ${item.assignee}: ${item.reason}`)
|
|
57
|
+
.join('\n')
|
|
58
|
+
|
|
59
|
+
if (details) {
|
|
60
|
+
return createCliError(
|
|
61
|
+
`Failed to create task: ${payload.error || 'Invalid assignees'}\n${details}`,
|
|
62
|
+
httpStatus,
|
|
63
|
+
parsedPayload
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
Array.isArray(payload.missingRequiredFields) &&
|
|
70
|
+
payload.missingRequiredFields.length > 0
|
|
71
|
+
) {
|
|
72
|
+
const lines: string[] = [
|
|
73
|
+
`Failed to create task: ${payload.error || 'Dataset incompatible'}`,
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if (payload.versionNum !== undefined) {
|
|
77
|
+
lines.push(` Version: v${payload.versionNum} (${payload.versionId || 'unknown'})`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push(` Missing required fields: ${payload.missingRequiredFields.join(', ')}`)
|
|
81
|
+
|
|
82
|
+
if (typeof payload.incompatibleRowCount === 'number') {
|
|
83
|
+
lines.push(` Incompatible rows: ${payload.incompatibleRowCount}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
payload.missingRequiredFieldCounts &&
|
|
88
|
+
Object.keys(payload.missingRequiredFieldCounts).length > 0
|
|
89
|
+
) {
|
|
90
|
+
const fieldCounts = Object.entries(payload.missingRequiredFieldCounts)
|
|
91
|
+
.map(([field, count]) => ` ${field}: ${count} rows`)
|
|
92
|
+
.join('\n')
|
|
93
|
+
lines.push(` Missing field counts:\n${fieldCounts}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return createCliError(
|
|
97
|
+
lines.join('\n'),
|
|
98
|
+
httpStatus,
|
|
99
|
+
parsedPayload
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (typeof payload.error === 'string' && payload.error.length > 0) {
|
|
104
|
+
return createCliError(
|
|
105
|
+
`Failed to create task: ${payload.error}`,
|
|
106
|
+
httpStatus,
|
|
107
|
+
parsedPayload
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return createCliError(`Failed to create task: ${rawBody}`, httpStatus, parsedPayload)
|
|
115
|
+
}
|