@naturalcycles/nodejs-lib 15.139.0 → 15.141.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.139.0",
4
+ "version": "15.141.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@standard-schema/spec": "^1",
@@ -14,7 +14,7 @@
14
14
  "yaml": "^2"
15
15
  },
16
16
  "devDependencies": {
17
- "@naturalcycles/dev-lib": "20.70.0",
17
+ "@naturalcycles/dev-lib": "0.0.0",
18
18
  "typescript": "^7"
19
19
  },
20
20
  "exports": {
@@ -1,5 +1,6 @@
1
1
  import { execSync, spawn, spawnSync } from 'node:child_process'
2
2
  import type { StdioOptions } from 'node:child_process'
3
+ import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js'
3
4
  import { _since } from '@naturalcycles/js-lib/datetime/time.util.js'
4
5
  import { AppError } from '@naturalcycles/js-lib/error/error.util.js'
5
6
  import { _substringAfterLast } from '@naturalcycles/js-lib/string/string.util.js'
@@ -69,7 +70,7 @@ class Exec2 {
69
70
  const log = opt.log ?? true // by default log should be true, as we are printing the output
70
71
  // Copy, to not mutate the input options
71
72
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
72
- const started = Date.now() as UnixTimestampMillis
73
+ const started = nowUnixMillis()
73
74
  this.logStart(cmd, opt)
74
75
 
75
76
  const r = spawnSync(cmd, opt.args, {
@@ -118,7 +119,7 @@ class Exec2 {
118
119
  logStart: opt.logStart ?? opt.log ?? false,
119
120
  logFinish: opt.logFinish ?? opt.log ?? false,
120
121
  }
121
- const started = Date.now() as UnixTimestampMillis
122
+ const started = nowUnixMillis()
122
123
  this.logStart(cmd, opt)
123
124
 
124
125
  try {
@@ -177,7 +178,7 @@ class Exec2 {
177
178
  const log = opt.log ?? true // by default log should be true, as we are printing the output
178
179
  // Copy, to not mutate the input options
179
180
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
180
- const started = Date.now() as UnixTimestampMillis
181
+ const started = nowUnixMillis()
181
182
  this.logStart(cmd, opt)
182
183
 
183
184
  await new Promise<void>((resolve, reject) => {
@@ -245,7 +246,7 @@ class Exec2 {
245
246
  const log = opt.log ?? printWhileRunning // by default log should be true, as we are printing the output
246
247
  // Copy, to not mutate the input options
247
248
  opt = { ...opt, log, logStart: opt.logStart ?? log, logFinish: opt.logFinish ?? log }
248
- const started = Date.now() as UnixTimestampMillis
249
+ const started = nowUnixMillis()
249
250
  this.logStart(cmd, opt)
250
251
  let stdout = ''
251
252
  let stderr = ''
package/src/fs/kpy.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path'
2
- import { _since, localTime } from '@naturalcycles/js-lib/datetime'
2
+ import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime'
3
3
  import type { UnixTimestampMillis } from '@naturalcycles/js-lib/types'
4
4
  import { glob, globSync } from 'tinyglobby'
5
5
  import { boldWhite, dimGrey, grey, yellow } from '../colors/colors.js'
@@ -45,7 +45,7 @@ export interface KpyOptions {
45
45
  }
46
46
 
47
47
  export async function kpy(opt: KpyOptions): Promise<void> {
48
- const started = localTime.nowUnixMillis()
48
+ const started = nowUnixMillis()
49
49
 
50
50
  opt = kpyPrepare(opt)
51
51
 
@@ -84,7 +84,7 @@ export async function kpy(opt: KpyOptions): Promise<void> {
84
84
  }
85
85
 
86
86
  export function kpySync(opt: KpyOptions): void {
87
- const started = localTime.nowUnixMillis()
87
+ const started = nowUnixMillis()
88
88
 
89
89
  opt = kpyPrepare(opt)
90
90
 
@@ -263,6 +263,7 @@ export class JWTService<T extends AnyObject = AnyObject> {
263
263
  const { payload } = await jwtVerify(token, key, {
264
264
  algorithms: this.cfg.verifyAlgorithms || [this.cfg.algorithm],
265
265
  ...joseOpt,
266
+ // oxlint-disable-next-line no-restricted-globals -- jose expects a Date
266
267
  currentDate: now === undefined ? undefined : new Date(now * 1000),
267
268
  })
268
269
  data = payload as TT
@@ -168,8 +168,8 @@ export class ProgressLogger<T> implements Disposable {
168
168
  private peakRSS!: number
169
169
 
170
170
  private start(): void {
171
- this.started = Date.now() as UnixTimestampMillis
172
- this.lastSecondStarted = Date.now() as UnixTimestampMillis
171
+ this.started = localTime.nowUnixMillis()
172
+ this.lastSecondStarted = localTime.nowUnixMillis()
173
173
  this.sma = new SimpleMovingAverage(10)
174
174
  this.processedLastSecond = 0
175
175
  this.progress = 0
@@ -213,7 +213,7 @@ export class ProgressLogger<T> implements Disposable {
213
213
 
214
214
  const mem = process.memoryUsage()
215
215
 
216
- const now = Date.now() as UnixTimestampMillis
216
+ const now = localTime.nowUnixMillis()
217
217
  const batchedProgress = this.progress * chunkSize
218
218
  const lastRPS =
219
219
  (this.processedLastSecond * chunkSize) / ((now - this.lastSecondStarted) / 1000) || 0
@@ -1,7 +1,7 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import { _hc } from '@naturalcycles/js-lib'
3
3
  import type { AbortableSignal } from '@naturalcycles/js-lib'
4
- import { _since } from '@naturalcycles/js-lib/datetime'
4
+ import { _since, nowUnixMillis } from '@naturalcycles/js-lib/datetime'
5
5
  import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
6
6
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
7
7
  import type { DeferredPromise } from '@naturalcycles/js-lib/promise'
@@ -141,7 +141,7 @@ export function transformMap<IN = any, OUT = IN>(
141
141
  async transform(this: Transform, chunk: IN, _, cb) {
142
142
  // Initialize start time on first item
143
143
  if (started === 0) {
144
- started = Date.now() as UnixTimestampMillis
144
+ started = nowUnixMillis()
145
145
  lastWarmupCheck = started
146
146
  }
147
147
 
@@ -262,7 +262,7 @@ export function transformMap<IN = any, OUT = IN>(
262
262
  })
263
263
 
264
264
  function updateConcurrency(): void {
265
- const now = Date.now()
265
+ const now = nowUnixMillis()
266
266
  if (now - lastWarmupCheck < WARMUP_CHECK_INTERVAL_MS) return
267
267
  lastWarmupCheck = now
268
268
 
@@ -1,8 +1,9 @@
1
1
  import { Transform } from 'node:stream'
2
2
  import type { AbortableSignal } from '@naturalcycles/js-lib'
3
+ import { nowUnixMillis } from '@naturalcycles/js-lib/datetime/localTime.js'
3
4
  import { _anyToError, _assert, ErrorMode } from '@naturalcycles/js-lib/error'
4
5
  import { createCommonLoggerAtLevel } from '@naturalcycles/js-lib/log'
5
- import type { IndexedMapper, Predicate, UnixTimestampMillis } from '@naturalcycles/js-lib/types'
6
+ import type { IndexedMapper, Predicate } from '@naturalcycles/js-lib/types'
6
7
  import { END, SKIP } from '@naturalcycles/js-lib/types'
7
8
  import { yellow } from '../../colors/colors.js'
8
9
  import type { TransformOptions, TransformTyped } from '../stream.model.js'
@@ -78,7 +79,7 @@ export function transformMapSync<IN = any, OUT = IN>(
78
79
  signal,
79
80
  } = opt
80
81
 
81
- const started = Date.now() as UnixTimestampMillis
82
+ const started = nowUnixMillis()
82
83
  let index = -1
83
84
  let countOut = 0
84
85
  let isSettled = false
@@ -73,7 +73,7 @@ export function transformThrottle<T>(opt: TransformThrottleOptions): TransformTy
73
73
  // console.log('pausing now after', {item, count})
74
74
  lock = pDefer()
75
75
  logger.log(
76
- `${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (Date.now() - start))}`,
76
+ `${localTime.now().toPretty()} transformThrottle activated: ${count} items passed in ${_since(start)}, will pause for ${_ms(interval * 1000 - (localTime.nowUnixMillis() - start))}`,
77
77
  )
78
78
  }
79
79
 
@@ -87,7 +87,7 @@ export function transformThrottleByRSS<T>(
87
87
 
88
88
  if (!disabled && lastRSS > maxRSSBytes && !lock) {
89
89
  lock = pDefer()
90
- pausedSince = Date.now()
90
+ pausedSince = localTime.nowUnixMillis()
91
91
  logger.log(
92
92
  `${localTime.now().toPretty()} transformThrottleByRSS paused: RSS ${_mb(lastRSS)} > ${maxRSS} MB`,
93
93
  )
@@ -122,7 +122,7 @@ export function transformThrottleByRSS<T>(
122
122
  )
123
123
  lock!.resolve()
124
124
  lock = undefined
125
- } else if (pollTimeout && Date.now() - pausedSince >= pollTimeout) {
125
+ } else if (pollTimeout && localTime.nowUnixMillis() - pausedSince >= pollTimeout) {
126
126
  clearTimeout(rssCheckTimer)
127
127
  if (onPollTimeout === 'throw') {
128
128
  lock!.reject(
@@ -1,3 +1,4 @@
1
+ // oxlint-disable-next-line no-restricted-globals -- the worker entry stays dependency-free
1
2
  const started = Date.now()
2
3
  import { inspect } from 'node:util'
3
4
  import { workerData, parentPort } from 'node:worker_threads'
@@ -20,6 +21,7 @@ try {
20
21
  const { WorkerClass } = await import(workerFile)
21
22
  const worker = new WorkerClass(workerData)
22
23
 
24
+ // oxlint-disable-next-line no-restricted-globals -- the worker entry stays dependency-free
23
25
  log(`${metric}#${workerIndex} loaded in ${Date.now() - started} ms`)
24
26
 
25
27
  let errors = 0
@@ -3,15 +3,14 @@ import { _assert } from '@naturalcycles/js-lib/error'
3
3
  import { _deepCopy, _mapObject, Set2 } from '@naturalcycles/js-lib/object'
4
4
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
5
5
  import type { AnyObject } from '@naturalcycles/js-lib/types'
6
- import type { Options, ValidateFunction } from 'ajv/dist/2020.js'
7
- import { Ajv2020 } from 'ajv/dist/2020.js'
6
+ import type { Code, KeywordCxt, Options, ValidateFunction } from 'ajv/dist/2020.js'
7
+ import { _, Ajv2020, str } from 'ajv/dist/2020.js'
8
8
  import { validTLDs } from '../tlds.js'
9
9
  import type {
10
10
  CustomConverterFn,
11
11
  CustomValidatorFn,
12
12
  JSchema,
13
13
  JsonSchemaIsoDateOptions,
14
- JsonSchemaIsoMonthOptions,
15
14
  JsonSchemaStringEmailOptions,
16
15
  JsonSchemaUtcOffsetOptions,
17
16
  } from './jSchema.js'
@@ -310,140 +309,79 @@ export function createAjv(opt?: Options): Ajv2020 {
310
309
  },
311
310
  })
312
311
 
312
+ // The date/time keywords are `code` keywords, which Ajv inlines into the compiled validator.
313
+ // A `validate` keyword is a function call with a context object, whose instancePath Ajv builds
314
+ // on every call (escaping the key of every map entry), even for valid data.
313
315
  ajv.addKeyword({
314
316
  keyword: 'IsoDate',
315
317
  type: 'string',
316
- modifying: false,
317
- errors: true,
318
318
  schemaType: 'object',
319
- validate: function validate(opt: JsonSchemaIsoDateOptions, data: string, _schema, ctx) {
320
- const hasOptions = Object.keys(opt).length > 0
321
-
322
- const isValid = isIsoDateValid(data)
323
- if (!isValid) {
324
- ;(validate as any).errors = [
325
- {
326
- instancePath: ctx?.instancePath ?? '',
327
- message: `is an invalid IsoDate`,
328
- },
329
- ]
330
-
331
- return false
332
- }
333
-
334
- if (!hasOptions) return true
335
-
336
- const { before, sameOrBefore, after, sameOrAfter } = opt
337
- const errors: any[] = []
338
-
339
- if (before) {
340
- const isParamValid = isIsoDateValid(before)
341
- const isRuleValid = isParamValid && before > data
342
-
343
- if (!isRuleValid) {
344
- errors.push({
345
- instancePath: ctx?.instancePath ?? '',
346
- message: `is not before ${before}`,
347
- })
348
- }
349
- }
350
-
351
- if (sameOrBefore) {
352
- const isParamValid = isIsoDateValid(sameOrBefore)
353
- const isRuleValid = isParamValid && sameOrBefore >= data
354
-
355
- if (!isRuleValid) {
356
- errors.push({
357
- instancePath: ctx?.instancePath ?? '',
358
- message: `is not the same or before ${sameOrBefore}`,
359
- })
360
- }
361
- }
362
-
363
- if (after) {
364
- const isParamValid = isIsoDateValid(after)
365
- const isRuleValid = isParamValid && after < data
366
-
367
- if (!isRuleValid) {
368
- errors.push({
369
- instancePath: ctx?.instancePath ?? '',
370
- message: `is not after ${after}`,
371
- })
372
- }
373
- }
374
-
375
- if (sameOrAfter) {
376
- const isParamValid = isIsoDateValid(sameOrAfter)
377
- const isRuleValid = isParamValid && sameOrAfter <= data
378
-
379
- if (!isRuleValid) {
380
- errors.push({
381
- instancePath: ctx?.instancePath ?? '',
382
- message: `is not the same or after ${sameOrAfter}`,
383
- })
384
- }
385
- }
386
-
387
- if (errors.length === 0) return true
388
- ;(validate as any).errors = errors
389
- return false
319
+ error: {
320
+ message: ({ params }) =>
321
+ params['rule']
322
+ ? str`is not ${isoDateRuleLabels[params['rule'] as IsoDateRule]} ${params['limit']}`
323
+ : str`is an invalid IsoDate`,
324
+ params: ({ params }) => (params['rule'] ? _`{${params['rule']}: ${params['limit']}}` : _`{}`),
325
+ },
326
+ code(cxt: KeywordCxt) {
327
+ const { gen, data } = cxt
328
+ const opt: JsonSchemaIsoDateOptions = cxt.schema
329
+ const isValid = gen.scopeValue('func', { ref: isIsoDateValid })
330
+
331
+ gen.if(
332
+ _`!${isValid}(${data})`,
333
+ () => cxt.error(),
334
+ () => {
335
+ for (const rule of isoDateRules) {
336
+ const limit = opt[rule]
337
+ if (!limit) continue
338
+
339
+ const params = { rule, limit }
340
+ if (!isIsoDateValid(limit)) {
341
+ // An invalid limit rejects every date
342
+ cxt.error(false, params)
343
+ continue
344
+ }
345
+ gen.if(isoDateRuleFails(rule, data, limit), () => cxt.error(false, params))
346
+ }
347
+ },
348
+ )
390
349
  },
391
350
  })
392
351
 
393
352
  ajv.addKeyword({
394
353
  keyword: 'IsoDateTime',
395
354
  type: 'string',
396
- modifying: false,
397
- errors: true,
398
355
  schemaType: 'boolean',
399
- validate: function validate(_opt: JsonSchemaIsoMonthOptions, data: string, _schema, ctx) {
400
- const isValid = isIsoDateTimeValid(data)
401
- if (isValid) return true
402
- ;(validate as any).errors = [
403
- {
404
- instancePath: ctx?.instancePath ?? '',
405
- message: `is an invalid IsoDateTime`,
406
- },
407
- ]
408
- return false
356
+ error: { message: 'is an invalid IsoDateTime' },
357
+ code(cxt: KeywordCxt) {
358
+ const isValid = cxt.gen.scopeValue('func', { ref: isIsoDateTimeValid })
359
+ cxt.fail(_`!${isValid}(${cxt.data})`)
409
360
  },
410
361
  })
411
362
 
412
363
  ajv.addKeyword({
413
364
  keyword: 'IsoMonth',
414
365
  type: 'string',
415
- modifying: false,
416
- errors: true,
417
366
  schemaType: 'object',
418
- validate: function validate(_opt: true, data: string, _schema, ctx) {
419
- const isValid = isIsoMonthValid(data)
420
- if (isValid) return true
421
- ;(validate as any).errors = [
422
- {
423
- instancePath: ctx?.instancePath ?? '',
424
- message: `is an invalid IsoMonth`,
425
- },
426
- ]
427
- return false
367
+ error: { message: 'is an invalid IsoMonth' },
368
+ code(cxt: KeywordCxt) {
369
+ const isValid = cxt.gen.scopeValue('func', { ref: isIsoMonthValid })
370
+ cxt.fail(_`!${isValid}(${cxt.data})`)
428
371
  },
429
372
  })
430
373
 
431
374
  ajv.addKeyword({
432
375
  keyword: 'UtcOffset',
433
376
  type: 'string',
434
- modifying: false,
435
- errors: true,
436
377
  schemaType: 'object',
437
- validate: function validate(opt: JsonSchemaUtcOffsetOptions, data: string, _schema, ctx) {
438
- const isValid = data === 'Z' ? opt.allowZ : data !== '' && isIsoTimezoneValid(data)
439
- if (isValid) return true
440
- ;(validate as any).errors = [
441
- {
442
- instancePath: ctx?.instancePath ?? '',
443
- message: `is an invalid UtcOffset`,
444
- },
445
- ]
446
- return false
378
+ error: { message: 'is an invalid UtcOffset' },
379
+ code(cxt: KeywordCxt) {
380
+ const opt: JsonSchemaUtcOffsetOptions = cxt.schema
381
+ const isValid = cxt.gen.scopeValue('func', {
382
+ ref: opt.allowZ ? isUtcOffsetOrZValid : isUtcOffsetValid,
383
+ })
384
+ cxt.fail(_`!${isValid}(${cxt.data})`)
447
385
  },
448
386
  })
449
387
 
@@ -496,39 +434,69 @@ export function createAjv(opt?: Options): Ajv2020 {
496
434
  },
497
435
  })
498
436
 
437
+ // Strips the keys that don't match the key schema; the values are validated by `additionalProperties`.
438
+ // The key schema is inlined per key with error reporting off, and the error count is reset after
439
+ // a mismatch, so a stripped key never surfaces as an error. Keys listed in `properties` are skipped.
499
440
  ajv.addKeyword({
500
441
  keyword: 'keySchema',
501
442
  type: 'object',
502
- modifying: true,
503
- errors: false,
504
443
  schemaType: 'object',
505
- compile(innerSchema, _parentSchema, _it) {
506
- const isValidKeyFn: ValidateFunction = ajv.compile(innerSchema)
444
+ trackErrors: true,
445
+ code(cxt: KeywordCxt) {
446
+ const { gen, data, parentSchema } = cxt
447
+ const propertyNames = Object.keys(parentSchema['properties'] || {})
448
+ const valid = gen.name('valid')
449
+
450
+ gen.forIn('key', data, key => {
451
+ const checkKey = (): void => {
452
+ cxt.subschema(
453
+ {
454
+ keyword: 'keySchema',
455
+ data: key,
456
+ dataTypes: ['string'],
457
+ compositeRule: true,
458
+ createErrors: false,
459
+ allErrors: false,
460
+ },
461
+ valid,
462
+ )
463
+ gen.if(_`!${valid}`, () => {
464
+ cxt.reset()
465
+ gen.code(_`delete ${data}[${key}]`)
466
+ })
467
+ }
507
468
 
508
- function validate(data: AnyObject, _ctx: any): boolean {
509
- if (typeof data !== 'object' || data === null) return true
469
+ if (!propertyNames.length) return checkKey()
510
470
 
511
- for (const key of Object.keys(data)) {
512
- if (!isValidKeyFn(key)) {
513
- delete data[key]
514
- }
471
+ let isNotProperty: Code = _`${key} !== ${propertyNames[0]}`
472
+ for (const name of propertyNames.slice(1)) {
473
+ isNotProperty = _`${isNotProperty} && ${key} !== ${name}`
515
474
  }
475
+ gen.if(isNotProperty, checkKey)
476
+ })
477
+ },
478
+ })
516
479
 
517
- return true
518
- }
519
-
520
- return validate
480
+ // Validates the inner schema only when the value is defined. Used for the optional values of
481
+ // `stringMap`/`record`, whose `additionalProperties` loop visits the keys with undefined values too.
482
+ ajv.addKeyword({
483
+ keyword: 'ifDefined',
484
+ schemaType: ['object', 'boolean'],
485
+ code(cxt: KeywordCxt) {
486
+ const { gen, data } = cxt
487
+ gen.if(_`${data} !== undefined`, () => {
488
+ cxt.subschema({ keyword: 'ifDefined' }, gen.name('valid'))
489
+ })
521
490
  },
522
491
  })
523
492
 
524
- // Validates that the value is undefined. Used in record/stringMap with optional value schemas
525
- // to allow undefined values in patternProperties via anyOf.
493
+ // Validates that the value is undefined. Superseded by `ifDefined`, kept for raw JSON schemas.
526
494
  ajv.addKeyword({
527
495
  keyword: 'isUndefined',
528
- modifying: false,
529
- errors: false,
530
496
  schemaType: 'boolean',
531
- validate: (_schema: boolean, data: unknown) => data === undefined,
497
+ code(cxt: KeywordCxt) {
498
+ cxt.fail(_`${cxt.data} !== undefined`)
499
+ },
532
500
  })
533
501
 
534
502
  // This is added because Ajv validates the `min/maxProperties` before validating the properties.
@@ -777,6 +745,27 @@ export function createAjv(opt?: Options): Ajv2020 {
777
745
  return ajv
778
746
  }
779
747
 
748
+ type IsoDateRule = keyof JsonSchemaIsoDateOptions
749
+
750
+ const isoDateRules: IsoDateRule[] = ['before', 'sameOrBefore', 'after', 'sameOrAfter']
751
+
752
+ const isoDateRuleLabels: Record<IsoDateRule, string> = {
753
+ before: 'before',
754
+ sameOrBefore: 'the same or before',
755
+ after: 'after',
756
+ sameOrAfter: 'the same or after',
757
+ }
758
+
759
+ /**
760
+ * IsoDate strings compare chronologically as plain strings.
761
+ */
762
+ function isoDateRuleFails(rule: IsoDateRule, data: Code, limit: string): Code {
763
+ if (rule === 'before') return _`${data} >= ${limit}`
764
+ if (rule === 'sameOrBefore') return _`${data} > ${limit}`
765
+ if (rule === 'after') return _`${data} <= ${limit}`
766
+ return _`${data} < ${limit}`
767
+ }
768
+
780
769
  const monthLengths = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
781
770
 
782
771
  const DASH_CODE = '-'.charCodeAt(0)
@@ -904,6 +893,14 @@ function isIsoTimezoneValid(s: string): boolean {
904
893
  return true
905
894
  }
906
895
 
896
+ function isUtcOffsetValid(s: string): boolean {
897
+ return s !== 'Z' && s !== '' && isIsoTimezoneValid(s)
898
+ }
899
+
900
+ function isUtcOffsetOrZValid(s: string): boolean {
901
+ return s === 'Z' || (s !== '' && isIsoTimezoneValid(s))
902
+ }
903
+
907
904
  /**
908
905
  * This is a performance optimized correct validation
909
906
  * for ISO month formatted as "YYYY-MM".