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.
@@ -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
+ }