Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
6a1d3b0
Bootstrapped notebooks
roll May 29, 2025
02b81d0
Removed decorators from tsconfig
roll May 29, 2025
71c3785
Added polars dependency
roll May 29, 2025
93b75a5
Bootstrapped functions
roll May 29, 2025
89693f8
Bootstraped fromPolars
roll May 29, 2025
1ee1519
Bootstrapped toPolars
roll May 29, 2025
ec7615b
Fixed formatting
roll May 29, 2025
cf12cc4
Merge branch 'main' into implement-table-package
roll Jun 2, 2025
bff8af2
Renamed datapackage to dataPackage in API
roll Jun 2, 2025
177920e
Bootstrapped inline reading
roll Jun 3, 2025
f5ab56c
Implemented readInlineResourceTable
roll Jun 3, 2025
a0e6872
Fixed short columns padding
roll Jun 3, 2025
ec22b0e
Simplified tests
roll Jun 3, 2025
50884f0
Extracted field module in core
roll Jun 3, 2025
67bcb30
Extracted table/field module
roll Jun 3, 2025
15f5619
Bootstrapped read/write cell
roll Jun 3, 2025
882a571
Bootstrapped readCell tests
roll Jun 3, 2025
e2a837f
Implemented parseIntegerColumn
roll Jun 3, 2025
d0a3aab
Removed readCell
roll Jun 3, 2025
914d431
Implemented parseNumberColumn
roll Jun 3, 2025
b81e11e
Fixed linting
roll Jun 3, 2025
1cf934c
Bootstrapped boolean column type
roll Jun 4, 2025
880cc39
Rebased integer parser on expr
roll Jun 4, 2025
ac626e8
Rebased number on expr
roll Jun 4, 2025
323b99f
Added field missing values support
roll Jun 4, 2025
d771d24
Added parseColumn spec
roll Jun 4, 2025
531c578
Fixed boolean type
roll Jun 4, 2025
2670340
Implemented year type
roll Jun 4, 2025
11959fc
Implemented date column
roll Jun 4, 2025
2e3cf5e
Implemented datetime column type
roll Jun 4, 2025
38c5694
Implemented time column type
roll Jun 4, 2025
dc4daf9
Removed AI.md
roll Jun 4, 2025
9b8fd5e
Implemented processTable
roll Jun 4, 2025
5c73ff9
Removed unused table code
roll Jun 4, 2025
95d4a9a
Split table/inline packages
roll Jun 4, 2025
7a5f022
Suppored v2 fieldsMatch
roll Jun 5, 2025
c030c35
Implemented fieldMatch logic
roll Jun 5, 2025
a09e603
Merged draft/readTable
roll Jun 5, 2025
9c0e7e1
Fixed readInlineTable
roll Jun 5, 2025
736cb39
Added processTable sampleSize
roll Jun 5, 2025
21808f4
Bootstrapped inferSchema
roll Jun 5, 2025
f04f5ae
Bootstraped regex matching
roll Jun 5, 2025
f4f4a03
Improved infer performance
roll Jun 5, 2025
1127c3d
Added date infer
roll Jun 5, 2025
83039c5
Added time support
roll Jun 5, 2025
ee770bb
Added datetime support
roll Jun 5, 2025
88866a2
Bootstrapped validateColumn
roll Jun 6, 2025
b7434c5
Renamed DescriptorError to MetadataError
roll Jun 6, 2025
17c10b5
Bootstrapped TableError
roll Jun 6, 2025
f76d1e0
Rebased process tests on it
roll Jun 6, 2025
497e0e4
Bootstrapped validateStructure
roll Jun 6, 2025
3945e62
Improved PolarsSchema interface
roll Jun 6, 2025
eda2843
Implemented equal
roll Jun 6, 2025
3617c18
Implemented validateStructure
roll Jun 6, 2025
9f650c5
Added validate spec
roll Jun 6, 2025
0f420a5
Improved core function API
roll Jun 6, 2025
ad93ece
Updated camtrap
roll Jun 6, 2025
870a5ba
Updated file
roll Jun 6, 2025
3b78fc0
Updated ckan
roll Jun 6, 2025
0622751
Updated zip
roll Jun 6, 2025
cac6165
Updated zenodo
roll Jun 6, 2025
ff43953
Updated github
roll Jun 6, 2025
a4e0037
Update dpkit
roll Jun 6, 2025
900ec1f
Updated table
roll Jun 6, 2025
34345cf
Updated claude
roll Jun 6, 2025
9b8da41
Split file/folder packages
roll Jun 6, 2025
fdf7a68
Updated docs
roll Jun 6, 2025
b0f040f
Added object type
roll Jun 6, 2025
7db8af6
Added array/geojson column types
roll Jun 6, 2025
774de36
Bootstrapped geopoint column type
roll Jun 6, 2025
6cecd34
Bootstrapped yearmonth type
roll Jun 6, 2025
ed68ad3
Bootstrapped list type
roll Jun 6, 2025
af461ee
Added TODO
roll Jun 6, 2025
8e282ad
Bootstrapped duration column type
roll Jun 6, 2025
fbf9dae
Bootstrapped string parser
roll Jun 6, 2025
3c25336
Improved errors hierarchy
roll Jun 7, 2025
126ec3b
Renamed column to field
roll Jun 7, 2025
1fce331
Implemented field name/type validation
roll Jun 7, 2025
5181cc5
Implroved cell validation approach
roll Jun 7, 2025
eac32af
Fixed fields validation
roll Jun 7, 2025
dbf5309
Added checks
roll Jun 7, 2025
b11d993
Added TODO
roll Jun 7, 2025
356efa4
Renamed check functions
roll Jun 7, 2025
3583a8a
Implemented minimum/maximum checks
roll Jun 7, 2025
3d70e4a
Implemented minLength, maxLength checks
roll Jun 7, 2025
ff1ad35
Implemented pattern check
roll Jun 7, 2025
febe062
Added unique field check
roll Jun 7, 2025
273218f
Added enum check
roll Jun 7, 2025
85e6a5f
Added TODO
roll Jun 7, 2025
2ec44a6
Bootstrapped validateChecks
roll Jun 8, 2025
d8ae86e
Added RowUniqueError
roll Jun 8, 2025
0df4399
Split fileds errors
roll Jun 8, 2025
2a82994
Bootstrapped type checks
roll Jun 8, 2025
2c871d7
Fixed error collection
roll Jun 8, 2025
1dee344
Started rebasing on validateChecks
roll Jun 8, 2025
4c98de0
Extracted match field
roll Jun 9, 2025
063d0fd
Enabled type spec
roll Jun 9, 2025
e6e3d52
Rebased on validateFields
roll Jun 9, 2025
badd4d7
Migrated required
roll Jun 9, 2025
b0ab68a
Migrated pattern
roll Jun 9, 2025
211ffb4
Migrated enum
roll Jun 9, 2025
bb3f296
Migrated maximum
roll Jun 9, 2025
6ee294b
Migrated minimum
roll Jun 9, 2025
2147d31
Migrated max/minLength
roll Jun 9, 2025
a06c98c
Migrated unique
roll Jun 9, 2025
569f46b
Removed legacy checks
roll Jun 9, 2025
d179941
Added TODO
roll Jun 9, 2025
e85dbaa
Moved checks to fields
roll Jun 9, 2025
7be96af
Updated validate spec
roll Jun 10, 2025
02064b4
Rebased parseField specs on high-level execution
roll Jun 10, 2025
b35f40d
Drop targets from errorTable
roll Jun 10, 2025
34f14d6
Renamed column to errorName
roll Jun 10, 2025
682fdcb
Simplified field parse funcitons API
roll Jun 10, 2025
c8e7110
Fixed unique check
roll Jun 10, 2025
d19e520
Fixed missing values
roll Jun 10, 2025
7be4863
Implemented unique row validation
roll Jun 10, 2025
19075c3
Added datahub package
roll Jun 10, 2025
6c121fc
Added new packages to docs
roll Jun 10, 2025
8394aa1
Fixed data package redirects problem
roll Jun 10, 2025
d9a6520
Fixed inline
roll Jun 10, 2025
10ebc60
Added table overview
roll Jun 10, 2025
e13429f
Added inline overview
roll Jun 10, 2025
069da63
Fixed validateInlineTable
roll Jun 10, 2025
cfd542e
Added changeset
roll Jun 10, 2025
3534aff
Updated internal file names
roll Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/sweet-buttons-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@dpkit/camtrap": minor
"@dpkit/datahub": minor
"@dpkit/folder": minor
"@dpkit/github": minor
"@dpkit/inline": minor
"@dpkit/zenodo": minor
"dpkit": minor
"@dpkit/table": minor
"@dpkit/ckan": minor
"@dpkit/core": minor
"@dpkit/file": minor
"@dpkit/zip": minor
---

Implemented table/inline packages
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pids
# Storybook
*storybook.log

# Notebooks
.ipynb_checkpoints/

# React
.react-router/

Expand Down
2 changes: 0 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,3 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Comments:** You are forbiddedn to write `//` comments in the code
- **Todos:** If you asked to resolve TODO, follow the instructions literally
- **Linting:** Don't run linting as a part of your tasks
- **Functions:** Use one prope argument in functions instead of positional arguments

3 changes: 3 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"correctness": {
"useExhaustiveDependencies": "off"
},
"performance": {
"noDelete": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
Expand Down
16 changes: 7 additions & 9 deletions camtrap/package/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const SUPPORTED_PROFILES = [
/**
* Assert a Package descriptor (JSON Object) against its profile
*/
export async function assertCamtrapPackage(props: {
descriptor: Descriptor | Package
}) {
const descriptor = props.descriptor as Descriptor
export async function assertCamtrapPackage(
descriptorOrPackage: Descriptor | Package,
) {
const descriptor = descriptorOrPackage as Descriptor

const isCamtrap =
typeof descriptor.$schema === "string" &&
Expand All @@ -23,10 +23,8 @@ export async function assertCamtrapPackage(props: {
throw new Error(`Unsupported Camtrap profile "${descriptor.$schema}"`)
}

const { errors, datapackage } = await validatePackageDescriptor({
descriptor,
})
const { errors, dataPackage } = await validatePackageDescriptor(descriptor)

if (!datapackage) throw new AssertionError(errors)
return datapackage as CamtrapPackage
if (!dataPackage) throw new AssertionError(errors)
return dataPackage as CamtrapPackage
}
18 changes: 9 additions & 9 deletions ckan/general/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Descriptor } from "@dpkit/core"

export async function makeCkanApiRequest<T = Descriptor>(props: {
export async function makeCkanApiRequest<T = Descriptor>(options: {
ckanUrl: string
action: string
payload: Descriptor
Expand All @@ -10,22 +10,22 @@ export async function makeCkanApiRequest<T = Descriptor>(props: {
let body: string | FormData
const headers: Record<string, any> = {}

const url = new URL(props.ckanUrl)
url.pathname = `/api/3/action/${props.action}`
const url = new URL(options.ckanUrl)
url.pathname = `/api/3/action/${options.action}`

if (props.apiKey) {
headers.Authorization = props.apiKey
if (options.apiKey) {
headers.Authorization = options.apiKey
}

if (props.upload) {
if (options.upload) {
body = new FormData()
body.append("upload", props.upload.data, props.upload.name)
body.append("upload", options.upload.data, options.upload.name)

for (const [key, value] of Object.entries(props.payload)) {
for (const [key, value] of Object.entries(options.payload)) {
body.append(key, value)
}
} else {
body = JSON.stringify(props.payload)
body = JSON.stringify(options.payload)
headers["Content-Type"] = "application/json"
}

Expand Down
9 changes: 4 additions & 5 deletions ckan/package/load.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ describe("loadPackageFromCkan", () => {
useRecording()

it("should load a package", async () => {
const datapackage = await loadPackageFromCkan({
datasetUrl:
"https://data.nhm.ac.uk/dataset/join-the-dots-collection-level-descriptions",
})
const dataPackage = await loadPackageFromCkan(
"https://data.nhm.ac.uk/dataset/join-the-dots-collection-level-descriptions",
)

expect(datapackage).toMatchSnapshot()
expect(dataPackage).toMatchSnapshot()
})
})
22 changes: 8 additions & 14 deletions ckan/package/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { normalizeCkanPackage } from "./process/normalize.js"
* @param props Object containing the URL to the CKAN package
* @returns Package object and cleanup function
*/
export async function loadPackageFromCkan(props: {
datasetUrl: string
}) {
const { datasetUrl } = props

const packageId = extractPackageId({ datasetUrl })
export async function loadPackageFromCkan(datasetUrl: string) {
const packageId = extractPackageId(datasetUrl)
if (!packageId) {
throw new Error(`Failed to extract package ID from URL: ${datasetUrl}`)
}
Expand All @@ -34,7 +30,7 @@ export async function loadPackageFromCkan(props: {
}
}

const systemPackage = normalizeCkanPackage({ ckanPackage })
const systemPackage = normalizeCkanPackage(ckanPackage)
const userPackagePath = systemPackage.resources
.filter(resource => resource["ckan:key"] === "datapackage.json")
.map(resource => resource["ckan:url"])
Expand All @@ -58,28 +54,26 @@ export async function loadPackageFromCkan(props: {
* - https://open.africa/dataset/pib-annual-senegal
* - https://data.nhm.ac.uk/dataset/join-the-dots-collection-level-descriptions
*/
function extractPackageId(props: { datasetUrl: string }) {
const url = new URL(props.datasetUrl)
function extractPackageId(datasetUrl: string) {
const url = new URL(datasetUrl)
const pathParts = url.pathname.split("/").filter(Boolean)
return pathParts.at(-1)
}

/**
* Fetch resource schema data from CKAN datastore
*/
async function loadCkanSchema(props: {
async function loadCkanSchema(options: {
datasetUrl: string
resourceId: string
}) {
const { datasetUrl, resourceId } = props

try {
// For some reason, datastore_info doesn't work
// So we use data fetching endpoint that also returns the schema
const result = await makeCkanApiRequest({
ckanUrl: datasetUrl,
ckanUrl: options.datasetUrl,
action: "datastore_search",
payload: { resource_id: resourceId, limit: 0 },
payload: { resource_id: options.resourceId, limit: 0 },
})

// @ts-ignore
Expand Down
56 changes: 27 additions & 29 deletions ckan/package/process/denormalize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { normalizeCkanPackage } from "./normalize.js"
describe("denormalizeCkanPackage", () => {
it("denormalizes a Frictionless Data Package to a CKAN package", () => {
// Arrange
const datapackage: Package = {
const dataPackage: Package = {
name: "test-package",
title: "Test Package",
description: "This is a test package",
Expand Down Expand Up @@ -50,32 +50,32 @@ describe("denormalizeCkanPackage", () => {
}

// Act
const result = denormalizeCkanPackage({ datapackage })
const result = denormalizeCkanPackage(dataPackage)

// Assert
// Basic metadata
expect(result.name).toEqual(datapackage.name)
expect(result.title).toEqual(datapackage.title)
expect(result.notes).toEqual(datapackage.description)
expect(result.version).toEqual(datapackage.version)
//expect(result.metadata_created).toEqual(datapackage.created)
expect(result.name).toEqual(dataPackage.name)
expect(result.title).toEqual(dataPackage.title)
expect(result.notes).toEqual(dataPackage.description)
expect(result.version).toEqual(dataPackage.version)
//expect(result.metadata_created).toEqual(dataPackage.created)

// License
if (
datapackage.licenses &&
datapackage.licenses.length > 0 &&
datapackage.licenses[0]
dataPackage.licenses &&
dataPackage.licenses.length > 0 &&
dataPackage.licenses[0]
) {
const license = datapackage.licenses[0]
const license = dataPackage.licenses[0]
if (license.name) expect(result.license_id).toEqual(license.name)
if (license.title) expect(result.license_title).toEqual(license.title)
if (license.path) expect(result.license_url).toEqual(license.path)
}

// Contributors
if (datapackage.contributors && datapackage.contributors.length >= 2) {
const author = datapackage.contributors.find(c => c.role === "author")
const maintainer = datapackage.contributors.find(
if (dataPackage.contributors && dataPackage.contributors.length >= 2) {
const author = dataPackage.contributors.find(c => c.role === "author")
const maintainer = dataPackage.contributors.find(
c => c.role === "maintainer",
)

Expand All @@ -91,9 +91,9 @@ describe("denormalizeCkanPackage", () => {
}

// Tags
if (datapackage.keywords && datapackage.keywords.length > 0) {
expect(result.tags).toHaveLength(datapackage.keywords.length)
datapackage.keywords.forEach((keyword, index) => {
if (dataPackage.keywords && dataPackage.keywords.length > 0) {
expect(result.tags).toHaveLength(dataPackage.keywords.length)
dataPackage.keywords.forEach((keyword, index) => {
const tag = result.tags?.[index]
if (tag && keyword) {
expect(tag.name).toEqual(keyword)
Expand All @@ -103,15 +103,15 @@ describe("denormalizeCkanPackage", () => {
}

// Resources
expect(result.resources).toHaveLength(datapackage.resources.length)
expect(result.resources).toHaveLength(dataPackage.resources.length)

// Verify resources exist
expect(datapackage.resources.length).toBeGreaterThan(0)
expect(dataPackage.resources.length).toBeGreaterThan(0)
expect(result.resources?.length).toBeGreaterThan(0)

if (datapackage.resources.length > 0 && result.resources.length > 0) {
if (dataPackage.resources.length > 0 && result.resources.length > 0) {
// Verify first resource
const firstResource = datapackage.resources[0]
const firstResource = dataPackage.resources[0]
const firstCkanResource = result.resources[0]

expect(firstCkanResource).toBeDefined()
Expand All @@ -135,26 +135,26 @@ describe("denormalizeCkanPackage", () => {

it("handles empty resources array", () => {
// Arrange
const datapackage: Package = {
const dataPackage: Package = {
name: "test-package",
resources: [],
}

// Act
const result = denormalizeCkanPackage({ datapackage })
const result = denormalizeCkanPackage(dataPackage)

// Assert
expect(result.resources).toEqual([])
})

it("handles undefined optional properties", () => {
// Arrange
const datapackage: Package = {
const dataPackage: Package = {
resources: [], // Only required property
}

// Act
const result = denormalizeCkanPackage({ datapackage })
const result = denormalizeCkanPackage(dataPackage)

// Assert
expect(result.name).toBeUndefined()
Expand All @@ -178,12 +178,10 @@ describe("denormalizeCkanPackage", () => {
const originalCkanPackage = ckanPackageFixture as CkanPackage

// Act - First normalize to a Frictionless Data Package
const datapackage = normalizeCkanPackage({
ckanPackage: originalCkanPackage,
})
const dataPackage = normalizeCkanPackage(originalCkanPackage)

// Then denormalize back to a CKAN package
const resultCkanPackage = denormalizeCkanPackage({ datapackage })
const resultCkanPackage = denormalizeCkanPackage(dataPackage)

// Assert
// Basic metadata
Expand Down
34 changes: 15 additions & 19 deletions ckan/package/process/denormalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,38 @@ import type { CkanTag } from "../Tag.js"
* @param props Object containing the Package to denormalize
* @returns Denormalized CKAN Package object
*/
export function denormalizeCkanPackage(props: {
datapackage: Package
}) {
const { datapackage } = props

export function denormalizeCkanPackage(dataPackage: Package) {
const ckanPackage: SetRequired<Partial<CkanPackage>, "resources" | "tags"> = {
resources: [],
tags: [],
}

// Basic metadata
if (datapackage.name) ckanPackage.name = datapackage.name
if (datapackage.title) ckanPackage.title = datapackage.title
if (datapackage.description) ckanPackage.notes = datapackage.description
if (datapackage.version) ckanPackage.version = datapackage.version
if (dataPackage.name) ckanPackage.name = dataPackage.name
if (dataPackage.title) ckanPackage.title = dataPackage.title
if (dataPackage.description) ckanPackage.notes = dataPackage.description
if (dataPackage.version) ckanPackage.version = dataPackage.version

// Process license information
if (datapackage.licenses && datapackage.licenses.length > 0) {
const license = datapackage.licenses[0] // Use first license
if (dataPackage.licenses && dataPackage.licenses.length > 0) {
const license = dataPackage.licenses[0] // Use first license

if (license?.name) ckanPackage.license_id = license.name
if (license?.title) ckanPackage.license_title = license.title
if (license?.path) ckanPackage.license_url = license.path
}

// Process contributors
if (datapackage.contributors) {
if (dataPackage.contributors) {
// Find author (contributor with role 'author')
const author = datapackage.contributors.find(c => c.role === "author")
const author = dataPackage.contributors.find(c => c.role === "author")
if (author) {
ckanPackage.author = author.title
if (author.email) ckanPackage.author_email = author.email
}

// Find maintainer (contributor with role 'maintainer')
const maintainer = datapackage.contributors.find(
const maintainer = dataPackage.contributors.find(
c => c.role === "maintainer",
)
if (maintainer) {
Expand All @@ -55,19 +51,19 @@ export function denormalizeCkanPackage(props: {
}

// Process resources
if (datapackage.resources && datapackage.resources.length > 0) {
ckanPackage.resources = datapackage.resources
.map(resource => denormalizeCkanResource({ resource }))
if (dataPackage.resources && dataPackage.resources.length > 0) {
ckanPackage.resources = dataPackage.resources
.map(resource => denormalizeCkanResource(resource))
.filter((resource): resource is CkanResource => resource !== undefined)
}

// Process keywords as tags
if (datapackage.keywords && datapackage.keywords.length > 0) {
if (dataPackage.keywords && dataPackage.keywords.length > 0) {
// TODO: Rebase on something like CkanPackage / NewCkanPackage
// with NewCkanTag/Resource etc to separate metadata read from API
// and metadata that is mapped from Data Package
// @ts-ignore
ckanPackage.tags = datapackage.keywords.map(keyword => {
ckanPackage.tags = dataPackage.keywords.map(keyword => {
const tag: Partial<CkanTag> = {
name: keyword,
display_name: keyword,
Expand Down
Loading