From 816db6858ce6d2e647f3d9af2d359ae3fd2aeec1 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 9 Feb 2025 22:59:31 +0000 Subject: [PATCH] [site/content/docs/dms][s]: remove this directory as duplicate of content on tech.datopian.com/ especially tech.datopian.com/dms. --- site/content/docs/dms/authentication.md | 249 ---- site/content/docs/dms/blob-storage.md | 215 --- .../docs/dms/ckan-client-guide/index.md | 503 ------- .../content/docs/dms/ckan-enterprise/index.md | 108 -- site/content/docs/dms/ckan-v3/index.md | 365 ----- site/content/docs/dms/ckan-v3/next-gen.md | 203 --- site/content/docs/dms/ckan.md | 23 - .../content/docs/dms/ckan/create-extension.md | 162 --- site/content/docs/dms/ckan/faq.md | 110 -- site/content/docs/dms/ckan/getting-started.md | 77 - .../docs/dms/ckan/install-extension.md | 76 - site/content/docs/dms/ckan/play-around.md | 285 ---- site/content/docs/dms/cms-for-data-portals.md | 81 -- site/content/docs/dms/dashboards.md | 163 --- .../dms/dashboards/hdx-dashboards-notes.md | 358 ----- site/content/docs/dms/data-api.md | 270 ---- site/content/docs/dms/data-explorer.md | 282 ---- .../datastore-query-builder/index.md | 109 -- site/content/docs/dms/data-explorer/design.md | 145 -- site/content/docs/dms/data-lake.md | 18 - site/content/docs/dms/data-portals.md | 287 ---- site/content/docs/dms/dataframe.md | 158 --- site/content/docs/dms/datahub.md | 11 - site/content/docs/dms/datahub/developers.md | 99 -- .../docs/dms/datahub/developers/api.md | 34 - .../docs/dms/datahub/developers/deploy.md | 91 -- .../docs/dms/datahub/developers/platform.md | 209 --- .../docs/dms/datahub/developers/publish.md | 107 -- .../dms/datahub/developers/user-stories.md | 811 ----------- .../dms/datahub/developers/views-research.md | 1253 ----------------- .../docs/dms/datahub/developers/views.md | 168 --- site/content/docs/dms/datahub/v3.md | 184 --- site/content/docs/dms/dms.md | 87 -- site/content/docs/dms/flows.md | 94 -- site/content/docs/dms/flows/airtunnel.png | Bin 221659 -> 0 bytes site/content/docs/dms/flows/design.md | 225 --- site/content/docs/dms/flows/history.md | 517 ------- site/content/docs/dms/flows/research.md | 109 -- site/content/docs/dms/frictionless.md | 56 - site/content/docs/dms/frontend/index.md | 113 -- site/content/docs/dms/giftless.md | 190 --- site/content/docs/dms/glossary.md | 39 - site/content/docs/dms/harvesting.md | 658 --------- site/content/docs/dms/hubstore.md | 31 - site/content/docs/dms/index.md | 114 -- site/content/docs/dms/load.md | 183 --- site/content/docs/dms/load/design.md | 183 --- site/content/docs/dms/notebook/index.md | 593 -------- site/content/docs/dms/permissions.md | 60 - site/content/docs/dms/publish.md | 427 ------ site/content/docs/dms/publish/design.md | 287 ---- site/content/docs/dms/relationships.md | 27 - site/content/docs/dms/storage.md | 10 - site/content/docs/dms/versioning.md | 134 -- site/content/docs/dms/versioning/NOTES.md | 28 - site/content/docs/dms/versioning/design.md | 322 ----- site/content/docs/dms/versioning/developer.md | 162 --- site/content/docs/dms/views.md | 117 -- site/content/docs/dms/views/design.md | 119 -- 59 files changed, 12099 deletions(-) delete mode 100644 site/content/docs/dms/authentication.md delete mode 100644 site/content/docs/dms/blob-storage.md delete mode 100644 site/content/docs/dms/ckan-client-guide/index.md delete mode 100644 site/content/docs/dms/ckan-enterprise/index.md delete mode 100644 site/content/docs/dms/ckan-v3/index.md delete mode 100644 site/content/docs/dms/ckan-v3/next-gen.md delete mode 100644 site/content/docs/dms/ckan.md delete mode 100644 site/content/docs/dms/ckan/create-extension.md delete mode 100644 site/content/docs/dms/ckan/faq.md delete mode 100644 site/content/docs/dms/ckan/getting-started.md delete mode 100644 site/content/docs/dms/ckan/install-extension.md delete mode 100644 site/content/docs/dms/ckan/play-around.md delete mode 100644 site/content/docs/dms/cms-for-data-portals.md delete mode 100644 site/content/docs/dms/dashboards.md delete mode 100644 site/content/docs/dms/dashboards/hdx-dashboards-notes.md delete mode 100644 site/content/docs/dms/data-api.md delete mode 100644 site/content/docs/dms/data-explorer.md delete mode 100644 site/content/docs/dms/data-explorer/datastore-query-builder/index.md delete mode 100644 site/content/docs/dms/data-explorer/design.md delete mode 100644 site/content/docs/dms/data-lake.md delete mode 100644 site/content/docs/dms/data-portals.md delete mode 100644 site/content/docs/dms/dataframe.md delete mode 100644 site/content/docs/dms/datahub.md delete mode 100644 site/content/docs/dms/datahub/developers.md delete mode 100644 site/content/docs/dms/datahub/developers/api.md delete mode 100644 site/content/docs/dms/datahub/developers/deploy.md delete mode 100644 site/content/docs/dms/datahub/developers/platform.md delete mode 100644 site/content/docs/dms/datahub/developers/publish.md delete mode 100644 site/content/docs/dms/datahub/developers/user-stories.md delete mode 100644 site/content/docs/dms/datahub/developers/views-research.md delete mode 100644 site/content/docs/dms/datahub/developers/views.md delete mode 100644 site/content/docs/dms/datahub/v3.md delete mode 100644 site/content/docs/dms/dms.md delete mode 100644 site/content/docs/dms/flows.md delete mode 100644 site/content/docs/dms/flows/airtunnel.png delete mode 100644 site/content/docs/dms/flows/design.md delete mode 100644 site/content/docs/dms/flows/history.md delete mode 100644 site/content/docs/dms/flows/research.md delete mode 100644 site/content/docs/dms/frictionless.md delete mode 100644 site/content/docs/dms/frontend/index.md delete mode 100644 site/content/docs/dms/giftless.md delete mode 100644 site/content/docs/dms/glossary.md delete mode 100644 site/content/docs/dms/harvesting.md delete mode 100644 site/content/docs/dms/hubstore.md delete mode 100644 site/content/docs/dms/index.md delete mode 100644 site/content/docs/dms/load.md delete mode 100644 site/content/docs/dms/load/design.md delete mode 100644 site/content/docs/dms/notebook/index.md delete mode 100644 site/content/docs/dms/permissions.md delete mode 100644 site/content/docs/dms/publish.md delete mode 100644 site/content/docs/dms/publish/design.md delete mode 100644 site/content/docs/dms/relationships.md delete mode 100644 site/content/docs/dms/storage.md delete mode 100644 site/content/docs/dms/versioning.md delete mode 100644 site/content/docs/dms/versioning/NOTES.md delete mode 100644 site/content/docs/dms/versioning/design.md delete mode 100644 site/content/docs/dms/versioning/developer.md delete mode 100644 site/content/docs/dms/views.md delete mode 100644 site/content/docs/dms/views/design.md diff --git a/site/content/docs/dms/authentication.md b/site/content/docs/dms/authentication.md deleted file mode 100644 index a3720a80..00000000 --- a/site/content/docs/dms/authentication.md +++ /dev/null @@ -1,249 +0,0 @@ -# Authentication - -## Introduction - -The core function of authentication is to **Identify** Users of the Portal (in a federated way) so we can base access on their identity. - -There are 3 major conceptual components: Identity, Accounts and Sessions which come together in the following stages: - -* **Root Identity Determination:** Determine Identity often via Delegation -* **Sessions:** Persistence of the identity in the web application in a secure way (without new identity determination on each request! I don't want to have to login via third party service every time) -* **Account (aka profile):** Storing Related Account/Profile Information in our application (not in third party identity) eg. email, name (other preferences) - * This will get auto-created usually at first Identification - * In limited case this can be seen as a cache of info from Identity system (e.g. your email) - * However often richer info that is app specific that is generated (relevant for personalization) - -### Root Identity Determination options :key: - -The identity determination can be done in multiple ways. In this article we're considering following 3 options that we believe are widely used: - -- Password authentication - traditional username and password pair -- Single Sign-on (SSO) via protocols such as OAuth, SAML, OpenID Connect -- One-time password (OTP) via email or SMS (aka passwordless connection) - -#### Password authentication - -Traditional way of authentication of users. When signing up user provides at least username and password pair which is then stored in a database for future authentication processes. Normally, additional information such as email address, full name etc. is also requested when registering. - -Examples of password authentication in popular services: - -- GitHub - https://github.com/join -- GitLab - https://gitlab.com/users/sign_up -- NPM - https://www.npmjs.com/signup - -#### Single Sign-on (SSO) - -The way of delegating identity determination process to some third-party service. Normally, popular social network services are used, e.g., Google, Facebook, Twitter etc. SSO implementations can be done using OAuth or SAML protocols. In addition, there is OpenID Connect protocol which is an extension of OAuth2.0. - -- OAuth - - JWT based - - JSON based - - 'webby' -- SAML - - XML based - - SOAP based - - 'enterprisey' - -List of OAuth providers: - -https://en.wikipedia.org/wiki/List_of_OAuth_providers - -Examples of SSO in popular projects: - -- https://datahub.io/login -- https://vercel.com/signup - -#### One-time password (OTP) - -Also known as dynamic password, OTP also solves limitations of traditional password authentication method. Usually, the one time passwords are received via email or SMS. - -### Account (aka profile) - -- Storage of user profile information (email, fullname, gravatar etc.) -- Retrieving user profile information via API -- Updating profile -- Deleting profile - -### Sessions - -- Log out: DePersisting the Session -- Invalidating all Sessions: e.g. if a security issue -- Sessions outside of browsers - -## Key Job Stories - -When a user signs in, I want to know her/his identity so that I can limit access and editing based on who she/he is. - -When a user visits the data portal for the first time, I want to provide him/her a way to register easily/quickly so that more people uses the data portal. - -When I visit the data portal for the first time, I want to sign up using my existing social network account so that I don't need to remember yet another credentials. - -When I'm using the CLI app (or anything else outside browser), I want to be able to login so that I can work from the terminal (e.g., have write access: editing datasets etc.). - -[More job stories](#more-job-stories). - -## CKAN 2 (CKAN Classic) - -### Basic CKAN authentication - -In classic system, we have basic CKAN authentication. Below is how registration page looks like: - -![CKAN Classic register page](/static/img/docs/dms/ckan-register.png) - -Registration flow in CKAN Classic: - -```mermaid -sequenceDiagram - - user->>ckan: fill in the form and submit - ckan->>ckan: check access (if user can create user) - ckan->>ckan: parse params - ckan->>ckan: check recaptcha - ckan->>ckan: call 'user_create' action - ckan->>ckan.model: add a new user into db - ckan->>ckan: create an activity - ckan->>ckan: log the user - ckan->>user: redirect to dashboard -``` - -We can extend basic CKAN authentication with: - -- LDAP - - https://extensions.ckan.org/extension/ldap/ - - https://github.com/NaturalHistoryMuseum/ckanext-ldap -- OAuth - see below -- SAML - https://extensions.ckan.org/extension/saml2/ - -### CKAN Classic as OAuth client - -CKAN Classic can also be used as OAuth client: - -- https://github.com/conwetlab/ckanext-oauth2 - this is the only one that's maintained. -- https://github.com/etalab/ckanext-oauth2 - outdated, the one above is based on this. -- https://github.com/okfn/ckanext-oauth - last commit 9 years ago. -- https://github.com/ckan/ckanext-oauth2waad - Windows Azure Active Directory specific and outdated. - -How it works: - -```mermaid -sequenceDiagram - - user->>ckan: request for login via OAuth provider - ckan->>ckan.oauth: raise 401 and call `challenge` function - ckan.oauth->>user: redirect the user to the 3rd party log in page - user->>3rdparty: perform login - 3rdparty->>ckan.oauth: redirect to /oauth2/callback with token - ckan.oauth->>3rdparty: call `authenticate` with token - 3rdparty->>ckan.oauth: return user info - ckan.oauth->>ckan: if doesn't exist save that info in db or update it - ckan.oauth->>ckan.oauth: add cookies - ckan.oauth->>user: redirect to dashboard -``` - -## CKAN 3 (Next Gen) - -We have considered some of popular and/or modern solutions for identity management that we can implement in CKAN 3: - -https://docs.google.com/spreadsheets/d/1qXZyzAbA2NtpnoSZRJ2K_EbaWJnvxkrKVzQ_2rD5eQw/edit#gid=0 - -Shortlist based on scores from the spreadsheet above: - -- Auth0 -- AuthN -- Ory/Kratos - -Recommendation: - -All projects from the shortlist can be considered for a project. It worth to give a try for each of them and find out what works best for your project's needs. Testing out Auth0 should be straightforward and take less than an hour. AuthN and Ory/Kratos would require to build docker images and to run it locally but overall it should not be time consuming. - -### Existing work - -In datahub.io we have implemented SSO via Google/Github. Below is sequence diagram showing the auth flow with datopian/auth + frontend express app (similar to CKAN 3 frontend): - -```mermaid -sequenceDiagram - - frontend.login->>auth.authenticate: authenticate(jwt=None,next=/success/...) - auth.authenticate->>frontend.login: failed + here are urls for logging on 3rd party including success - frontend.login->>user: login form with login urls to 3rd party including next url in state - user->>3rdparty: login - 3rdparty->>auth.oauth_response: success - auth.oauth_response->>frontend.success: redirect to next url - frontend.success->>auth.authenticate: with valid jwt - auth.authenticate->>frontend.success: valid + here is profile - frontend.success->>frontend.success: decode jwt, check it, then see localstorage - frontend.success->>frontend.dashboard: redirect to dashboard -``` - -## CKAN 2 to CKAN 3 (aka Next Gen) - -How does this conceptual framework map to an evolution of CKAN 2 to CKAN 3? - -```mermaid -graph TD - -subgraph "CKAN Classic" - Signup["Classic signup, e.g., self-service or by sysadmin"] - Login["Classic login if you're using the classic UI"] - OAuth["OAuth2(ORY/Hydra)"] -end - -subgraph "Authentication service (ORY/Kratos)" - SSO["Social Sign-On: Github, Google, Facebook"] - CC["CKAN Classic"] - Admins["Sysadmin users"] - Curators["Data curators"] - Users["Regular users"] -end - -subgraph "Frontend v3" - SignupFront["Signup via Kratos"] - LoginFront["Login via Kratos"] -end - -SignupFront --"Regular user"--> SSO -LoginFront --"Regular user"--> SSO - -LoginFront --"Data curator"--> CC - -CC --> Admins -CC --> Curators -SSO --> Users - -CC --"Redirect"--> OAuth -OAuth --> Login -``` - -Sequence diagram of login process: - -[![](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5cdEJyb3dzZXItPj5Gcm9udGVuZDogUmVxdWVzdCB0byBgL2F1dGgvbG9naW5gXG4gIEZyb250ZW5kLT4-S3JhdG9zOiBBdXRoIHJlcXVlc3RcbiAgS3JhdG9zLT4-QnJvd3NlcjogUmVkaXJlY3QgdG8gYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWAgcGFyYW1cbiAgQnJvd3Nlci0-PkZyb250ZW5kOiBHZXQgYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWBcbiAgRnJvbnRlbmQtPj5LcmF0b3M6IEZldGNoIGRhdGEgZm9yIHJlbmRlcmluZyB0aGUgZm9ybVxuICBLcmF0b3MtPj5Gcm9udGVuZDogTG9naW4gb3B0aW9uc1xuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlbmRlciB0aGUgbG9naW4gZm9ybSB3aXRoIGF2YWlsYWJsZSBvcHRpb25zXG4gIEJyb3dzZXItPj5Gcm9udGVuZDogU3VwcGx5IGZvcm0gZGF0YVxuICBGcm9udGVuZC0-PktyYXRvczogVmFsaWRhdGUgYW5kIGxvZ2luXG4gIEtyYXRvcy0-PkZyb250ZW5kOiBTZXQgc2Vzc2lvblxuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlZGlyZWN0IHRvIC9kYXNoYm9hcmRcblxuXG5cdFx0XHRcdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG5cdEJyb3dzZXItPj5Gcm9udGVuZDogUmVxdWVzdCB0byBgL2F1dGgvbG9naW5gXG4gIEZyb250ZW5kLT4-S3JhdG9zOiBBdXRoIHJlcXVlc3RcbiAgS3JhdG9zLT4-QnJvd3NlcjogUmVkaXJlY3QgdG8gYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWAgcGFyYW1cbiAgQnJvd3Nlci0-PkZyb250ZW5kOiBHZXQgYC9hdXRoL2xvZ2luP3JlcXVlc3Q9e2lkfWBcbiAgRnJvbnRlbmQtPj5LcmF0b3M6IEZldGNoIGRhdGEgZm9yIHJlbmRlcmluZyB0aGUgZm9ybVxuICBLcmF0b3MtPj5Gcm9udGVuZDogTG9naW4gb3B0aW9uc1xuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlbmRlciB0aGUgbG9naW4gZm9ybSB3aXRoIGF2YWlsYWJsZSBvcHRpb25zXG4gIEJyb3dzZXItPj5Gcm9udGVuZDogU3VwcGx5IGZvcm0gZGF0YVxuICBGcm9udGVuZC0-PktyYXRvczogVmFsaWRhdGUgYW5kIGxvZ2luXG4gIEtyYXRvcy0-PkZyb250ZW5kOiBTZXQgc2Vzc2lvblxuICBGcm9udGVuZC0-PkJyb3dzZXI6IFJlZGlyZWN0IHRvIC9kYXNoYm9hcmRcblxuXG5cdFx0XHRcdFx0IiwibWVybWFpZCI6eyJ0aGVtZSI6ImRlZmF1bHQifSwidXBkYXRlRWRpdG9yIjpmYWxzZX0) - -From ORY/Kratos: - -[![](https://mermaid.ink/img/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL2xvZ2luIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvbG9naW5cbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTdWJtaXQgTG9naW5cIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBMb2dpbiBJbmZvXG5cbiAgYWx0IExvZ2luIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFNldHMgc2Vzc2lvbiBjb29raWVcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgTG9naW4gZGF0YSBpbnZhbGlkXG4gICAgSy0tPj5COiBSZWRpcmVjdHMgdG8geW91ciBBcHBsaWNhaXRvbidzIC9sb2dpbiBlbmRwb2ludFxuICAgIEItPj5BOiBDYWxscyAvbG9naW5cbiAgICBBLS0-Pks6IEZldGNoZXMgZGF0YSB0byByZW5kZXIgZm9ybSBmaWVsZHMgYW5kIGVycm9yc1xuICAgIEItLT4-QTogRmlsbHMgb3V0IGZvcm1zIGFnYWluLCBjb3JyZWN0cyBlcnJvcnNcbiAgICBCLT4-SzogUE9TVHMgZGF0YSBhZ2FpbiAtIGFuZCBzbyBvbi4uLlxuICBlbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjowLCJub3RlTWFyZ2luIjoxNSwibWVzc2FnZU1hcmdpbiI6NDUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19fQ)](https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoic2VxdWVuY2VEaWFncmFtXG4gIHBhcnRpY2lwYW50IEIgYXMgQnJvd3NlclxuICBwYXJ0aWNpcGFudCBLIGFzIE9SWSBLcmF0b3NcbiAgcGFydGljaXBhbnQgQSBhcyBZb3VyIEFwcGxpY2F0aW9uXG5cblxuICBCLT4-SzogSW5pdGlhdGUgTG9naW5cbiAgSy0-PkI6IFJlZGlyZWN0cyB0byB5b3VyIEFwcGxpY2F0aW9uJ3MgL2xvZ2luIGVuZHBvaW50XG4gIEItPj5BOiBDYWxscyAvbG9naW5cbiAgQS0tPj5LOiBGZXRjaGVzIGRhdGEgdG8gcmVuZGVyIGZvcm1zIGV0Y1xuICBCLS0-PkE6IEZpbGxzIG91dCBmb3JtcywgY2xpY2tzIGUuZy4gXCJTdWJtaXQgTG9naW5cIlxuICBCLT4-SzogUE9TVHMgZGF0YSB0b1xuICBLLS0-Pks6IFByb2Nlc3NlcyBMb2dpbiBJbmZvXG5cbiAgYWx0IExvZ2luIGRhdGEgdmFsaWRcbiAgICBLLS0-PkI6IFNldHMgc2Vzc2lvbiBjb29raWVcbiAgICBLLT4-QjogUmVkaXJlY3RzIHRvIGUuZy4gRGFzaGJvYXJkXG4gIGVsc2UgTG9naW4gZGF0YSBpbnZhbGlkXG4gICAgSy0tPj5COiBSZWRpcmVjdHMgdG8geW91ciBBcHBsaWNhaXRvbidzIC9sb2dpbiBlbmRwb2ludFxuICAgIEItPj5BOiBDYWxscyAvbG9naW5cbiAgICBBLS0-Pks6IEZldGNoZXMgZGF0YSB0byByZW5kZXIgZm9ybSBmaWVsZHMgYW5kIGVycm9yc1xuICAgIEItLT4-QTogRmlsbHMgb3V0IGZvcm1zIGFnYWluLCBjb3JyZWN0cyBlcnJvcnNcbiAgICBCLT4-SzogUE9TVHMgZGF0YSBhZ2FpbiAtIGFuZCBzbyBvbi4uLlxuICBlbmRcbiIsIm1lcm1haWQiOnsidGhlbWUiOiJuZXV0cmFsIiwic2VxdWVuY2VEaWFncmFtIjp7ImRpYWdyYW1NYXJnaW5YIjoxNSwiZGlhZ3JhbU1hcmdpblkiOjE1LCJib3hUZXh0TWFyZ2luIjowLCJub3RlTWFyZ2luIjoxNSwibWVzc2FnZU1hcmdpbiI6NDUsIm1pcnJvckFjdG9ycyI6dHJ1ZX19fQ) - - -Kratos to Hydra in CKAN Classic: - -WIP - -Questions - -* Does CKAN Classic allow us to store arbitrary account information (are there "extras") -* How would we avoid having to support identity persistence, delegation etc in both NG frontend and Classic Admin UI? - * Can we share cookies (e.g. via using subdomains) -* How is login, identity determination etc done at least for frontend in DataHub.io -* Should account UI really be in NG frontend vs Classic Admin UI? -* how can we handle "invite a user" to my org set up ... (it's basically post processing after sign up ...) - -## Appendix - -### More job stories - -When a user visits the data portal, I want to provide multiple options for him/her to sign up so that I have more users registered and using the data portal. - -When a user needs to change his/her profile info, I want to make sure it is possible, so that I have the up-to-date information about users. - -When my personal info (email etc.) is changed, I want to edit it in my profile so that I provide up-to-date information about me and I receive messages (eg, notifications) properly. - -When I decide to stop using the data portal, I want to be able to delete my account, so that my personal details aren't stored in the service that I don't need anymore. diff --git a/site/content/docs/dms/blob-storage.md b/site/content/docs/dms/blob-storage.md deleted file mode 100644 index a6affb1c..00000000 --- a/site/content/docs/dms/blob-storage.md +++ /dev/null @@ -1,215 +0,0 @@ -# Blob Storage - -## Introduction - -DMS and data portals often need to *store* data as well as metadata. As such, they require a system for doing this. This page focuses on Blob Storage aka Bulk or Raw storage (see [storage](/docs/dms/storage) page for an overview of all types of storage). - -Blob storage is for storing "blobs" of data, that is a raw stream of bytes like files on a filesystem. For blob storage think local filesystem or cloud storage like S3, GCS, etc. - -Blob Storage in a DMS can be provided via: - -* Local file system: storing on disk or storage directly connected to the instance -* Cloud storage like S3, Google Cloud Storage, Azure storage etc - -Today, cloud storage would be the default in most cases. - -### Features - -* Storage: Persistent, cost-efficient storage -* Download: Fast, reliable download (possibly even with support for edge distribution) -* Upload: reliable and rapid upload - * Direct upload to (cloud) storage by clients i.e. without going via the DMS. Why? Because cloud storage has many features that it would be costly replicate (e.g. multipart, resumable etc), excellent performance and reliability for upload. It also cuts out the middleman of the DMS backend thereby saving bandwidth, reducing load on the DMS backend and improving performance - * Upload UI: having an excellent UI for doing upload. NB: this UI is considered part of the [publish feature](/docs/dms/publish) -* Cloud: integrate with cloud storage -* Permissions: restricting access to data stored in blob storage based on the permissions of the DMS. For example, if Joe does not have access to a dataset on the DMS he should not be able to access associated blob data in the storage system - -## Flows - -### Direct to Cloud Upload - -Want: Direct upload to cloud storage ... But you need to authorize that ... So give them a token from your app - -A sequence diagram illustrating the process for a direct to cloud upload: - -```mermaid -sequenceDiagram - -participant Browser as Client (Browser / Code) -participant Authz as Authz Server -participant BitStore as Storage Access Token Service -participant Storage as Cloud Storage - - Browser->>Authz: Give me a BitStore access token - Authz->>Browser: Token - Browser->>BitStore: Get a signed upload URL (access token, file metdata) - BitStore->>Browser: Signed URL - Browser->>Storage: Upload file (signed URL) - Storage->>Browser: OK (storage metadata) -``` - -Here's a more elaborate version showing storage of metadata into the MetaStore afterwards (and skipping the Authz service): - -```mermaid -sequenceDiagram - - participant browser as Client (Browser / Code) - participant vfts as MetaStore - participant bitstore as Storage Access Token Service - participant storage as Cloud Storage - - browser->>browser: Select files to upload - browser->>browser: calculate file hashes (if doing content addressable) - browser->bitstore: get signed URLs(file1.csv URL, file2.csv URL, auth info) - bitstore->>browser: signed URLs - browser->>storage: upload file1.csv - storage->>browser: OK - browser->>storage: upload file2.csv - storage->>browser: OK - browser->>browser: Compose datapackage.json - browser->>vfts: create dataset(datapackage.json, file1.csv pointer, file2.csv pointer, jwt token, ...) - vfts->>browser: OK -``` - -## CKAN 2 (Classic) - -Blob Storage is known as the FileStore in CKAN v2 and below. The default is local disk storage. - -There is support for cloud storage via a variety of extensions the most prominent of which is `ckanext-cloudstorage`: https://github.com/TkTech/ckanext-cloudstorage - -There are a variety of issues: - -* Cloud storage is not a first class citizen in CKAN: CKAN defaults to local file storage but cloud storage is the default in the world and has much better scalability, performance as well as integratability with cloud deployment -* The FileStore interface definition has a poor separation of concerns (for example, blob storage file paths is set in the FileStore component not in core CKAN) which makes it hard / hacky to extend and use for key use cases e.g. versioning. -* `ckanext-cloudstorage` (the default cloud storage extension) is ok but has many issues e.g. - * No direct to cloud upload: it uses CKAN backend as a middleman so all data must go via ckan backend - * Implements its own (sometimes unreliable) version of multipart upload (which means additional code which isn't as reliable as cloud storage providers interface) - * No access to advanced features such as resumability etc - -Generally, we at Datopian have seen a lot of issues around multipart / large file upload stability with clients and are still seeing issues when a lot of large files are uploaded via scripts. Fixing and refactoring code related to storage is very costly, and tends to result in client specific "hacks". - -## CKAN v3 - -An approach to blob storage that leverages cloud blob storage directly (i.e. without having to upload and serve all files via the CKAN web server), unlocking the performance characteristics of the storage backend directly. It is designed with a microservice approach and supports direct to cloud uploads and downloads. The key components are listed in the next section. You can read more about the overall design approach in the [design section below](#Design). - -It is backwards compatible with CKAN v2 and has been successfully deployed with CKAN v2.8 and v2.9. - -**Status: Production.** - -### Components - -* [ckanext-blob-storage](https://github.com/datopian/ckanext-blob-storage) (formerly known as ckanext-external-storage) - * Hooking CKAN to Giftless replacing resource storage - * Depends on giftless-client and ckanext-authz-service - * Doesn't implement IUploader - completely overrides upload / download routes for resources -* [Giftless](https://github.com/datopian/giftless) - Git LFS compatible implementation for storage with some extras on top. This hands out access tokens to store data in cloud storage. - * Docs at https://giftless.datopian.com - * Backends for Azure, Google Cloud Storage and local - * Multipart support (on top of standard LFS protocol) - * Accepts JWT tokens for authentication and authorization -* [ckanext-authz-service](https://github.com/datopian/ckanext-authz-service/) - This extension uses CKAN’s built-in authentication and authorization capabilities to: a) Generate JWT tokens and provide them via CKAN’s Web API to clients and b) Validate JWT tokens. - * Allows hooking CKAN's authentication and authorization capabilities to generate signed JWT tokens, to integrate with external systems - * Not specific for Giftless, but this is what it was built for -* [ckanext-asset-storage](https://github.com/datopian/ckanext-asset-storage) - this takes care of storing non-data assets e.g. organization images etc. - * CKAN IUploader for assets (not resources!) - * Pluggable backends - currently local and Azure - * Much cleaner than older implementations (ckanext-cloudstorage etc.) - -Clients: - -* [giftless-client-py](https://github.com/datopian/giftless-client) - Python client for Git LFS and Giftless-specific features - * Used by ckanext-blob-storage and other tools -* [giftless-client-js](https://github.com/datopian/giftless-client-js) - Javascript client for Git LFS and Giftless-specific features - * Used by ckanext-blob-storage and other tools for creating uploaders in the UI - -## Design - -### Purpose - -The goal of this project is to create a more **_flexible_** system for storing **_data files_** (AKA “resources”) for **_CKAN_ and _other implementations_** of a data portal so that CKAN can support versioning, large file upload (and great file upload UX), plug easily into cloud and local file storage backends and, in general, is easy to customize both for storage layer and for CKAN client code of that layer - -### Features - -* Do one thing and do it well: provide an API to store and retrieve files from storage, in a way that is pluggable into a micro-services based application and to existing CKAN (2.8 / 2.9) -* Does not force, and in fact is not aware of, a specific file naming logic (i.e. resource file names could be based on a user given name, a content hash, a revision ID or any mixture of these - it is up to the using system to decide) -* Does not force a specific storage backend; Should support Amazon S3, Azure Storage and local file storage in some way initially but in general backend should be pluggable -* Does not force a specific authentication scheme; Expects a signed JWT token, does not care who signed it and how the user got authenticated -* Does not force complex authorization scheme; Leave it to external system to do complex authorization if needed; - * By default, the system can work in an “admin party” mode where all authenticated users have full access to all files. This will be “good enough” for many DMS implementations including CKAN. - * Potentially, allow plugging in a more complex authorization logic that relies on JWT claims to perform granular authorization checks - -### For Data Files (i.e. Blobs) - -This system is about storing and providing access to blobs, or streams of bytes; It is not about providing access to the data stored within (i.e. it is not meant to replace CKAN’s datastore). - -### For CKAN – whilst not necessarily CKAN Specific - -While the system’s design should not be CKAN specific in any way, our current client needs require us to provide a CKAN extension that integrates with this system. - -CKAN’s current IUploader interface has been identified to be too narrow to provide the functionality required by complex projects (resource versioning, direct cloud uploads and downloads, large file support and multipart support). While some of these needs could be and have been “hacked” through the IUploader interface, the implementations have been over complex and hard to debug. - -Our goal should be to provide a CKAN extension that provides the following functionality directly: - -* Uploading and downloading resource files directly from the client if supported by the storage backend - * Multipart upload support if supported by storage backend - * Handling of signed URLs for uploads and private downloads - * Client side code for handling multipart uploads - * TBD: If storage backend does not support direct uploads / downloads, fall back to … - -In addition, this extension should provide an API for other extensions to do things like: - -* Set the file naming scheme (We need this for ckanext-versions) -* Lower level file access, e.g. move and delete files. We may need this in the future to optimize storage and deduplicate files as proposed for ckanext-versions - -In addition, this extension must “play nice” with common CKAN features such as the datastore extension and related datapusher / xloader extensions. - -### Usable For other DMS implementations - -There should be nothing in this system, except for the CKAN extension described above, that is specific to CKAN. That will allow to re-use and re-integrate this system as a micro-service in other DMS implementations such as ckan-ng and others. -In fact, the core part of this system should be a generic, abstract storage service with a light authorization layer. This could make it useful in a host of situations where storage micro-service is needed. - -### High Level Principles - -Common Principles - -* Uploads and downloads directly from cloud provides to browser -* Signed uploads / downloads - for private / authorized only data access -* Support for AWS, Azure and potentially GCP storage -* Support for local (non cloud) storage, potentially through a system like [https://min.io/](https://min.io/) -* Multipart / large file upload support (a few GB in size should be supported for Gates) -* Not opinionated about file naming / paths; Allow users to set file locations under some pre-defined patchs / buckets -* Client side support - browser widgets / code for uploading and downloading files / multipart uploads directly to different backends -* Well-documented flow for using from API (not browser) -* Provided API for deleting and moving files -* Provided API for accessing storage-level metadata (e.g. file MD5) (do we need this could be useful for processes that do things like deduplicate storage) -* Provided API for managing storage-level object level settings (e.g. “Content-disposition” / “Content-type” headers, etc.) -* Authorization based on some kind of portable scheme (JWT) - -CKAN integration specific (implemented as a CKAN extension) - -* JWT generation based on current CKAN user permissions -* Client widgets integration (or CKAN specific widgets) in right places in CKAN templates -* Hook into resource upload / download / deletion controllers in CKAN -* API to allow other extensions to control storage level object metadata (headers, path) -* API to allow other extensions to hook into lifecycle events - upload completion, download request, deletion etc. - - -### Components - -The Decoupled Storage solution should be split into several parts, with some parts being independent of others: - -* [External] Cloud Storage service (or API similar if local file system) e.g. S3, GCS, Azure Storage, Min.io (for local file system) -* Cloud Storage Access Service -* [External] Permissions Service for granting general permission tokens that give access to Cloud Storage Access Service - * JWT tokens can be generated by any party that has the right signing key. Thus, we can initially do without this if JWT signing is implemented as part of the CKAN extension -* Browser based Client for Cloud Storage (compatible with #1 and with different cloud vendors) -* CKAN extension that wraps the two parts above to provide a storage solution for CKAN - -### Questions - -* What is file structure in cloud ... i.e. What is the file path for uploaded files? Options: - * Client chooses a name/path - * Content addressable i.e. the name is given by the content? How? Use a hash.] - * Beauty of that: standard way to name things. The same thing has the same name (modulo collisions) - * Goes with versioning => same file = same name, diff file = diff name -* And do you enforce that from your app - * Request for token needs to include the destination file path diff --git a/site/content/docs/dms/ckan-client-guide/index.md b/site/content/docs/dms/ckan-client-guide/index.md deleted file mode 100644 index 2275c342..00000000 --- a/site/content/docs/dms/ckan-client-guide/index.md +++ /dev/null @@ -1,503 +0,0 @@ -# CKAN Client Guide - -Guide to interacting with [CKAN](/docs/dms/ckan) for power users such as data scientists, data engineers and data wranglers. - -This guide is about adding and managing data in CKAN programmatically and it assumes: - -* You are familiar with key concepts like metadata, data, etc. -* You are working programmatically with a programming language such as Python, JavaScript or R (_coming soon_). - -## Frictionless Formats - -Clients use [Frictionless formats](https://specs.frictionlessdata.io/) by default for describing dataset and resource objects passed to client methods. Internally, we then use the a *CKAN {'<=>'} Frictionless Mapper* (both [in JavaScript]( https://github.com/datopian/frictionless-ckan-mapper-js ) and [in Python](https://github.com/frictionlessdata/frictionless-ckan-mapper)) to convert objects to CKAN formats before calling the API. **Thus, you can use _Frictionless Formats_ by default with the client**. - ->[!tip]As CKAN moves to Frictionless to default this will gradually become unnecessary. - -## Quick start - -Most of this guide has Python programming language in mind, including its [convention regading using _snake case_ for instances and methods names](https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles). - -If needed, you can adapt the instructions to JavaScript and R (coming soon) by using _camel case_ instead — for example, if in the Python code we have `client.push_blob(…)`, in JavaScript it would be `client.pushBlob(…)`. - -### Prerequisites - -Install the client for your language of choice: - -* Python: https://github.com/datopian/ckan-client-py#install -* JavaScript: https://github.com/datopian/ckan-client-js#install -* R: _coming soon_ - -### Create a client - -#### Python - -```python -from ckanclient import Client - - -api_key = '771a05ad-af90-4a70-beea-cbb050059e14' -api_url = 'http://localhost:5000' -organization = 'datopian' -dataset = 'dailyprices' -lfs_url = 'http://localhost:9419' - -client = Client(api_url, organization, dataset, lfs_url) -``` - -#### JavaScript - -```javascript -const { Client } = require('ckanClient') - -apiKey = '771a05ad-af90-4a70-beea-cbb050059e14' -apiUrl = 'http://localhost:5000' -organization = 'datopian' -dataset = 'dailyprices' - -const client = Client(apiKey, organization, dataset, apiUrl) -``` - -### Upload a resource - -That is to say, upload a file, implicitly creating a new dataset. - -#### Python - -```python -from frictionless import describe - - -resource = describe('my-data.csv') -client.push_blob(resource) -``` - -### Create a new empty Dataset with metadata - -#### Python - -```python -client.create('my-data') -client.push(resource) -``` - -### Adding a resource to an existing Dataset - ->[!note]Not implemented yet. - - -```python -client.create('my-data') -client.push_resource(resource) -``` - -### Edit a Dataset's metadata - ->[!note]Not implemented yet. - - -```python -dataset = client.retrieve('sample-dataset') -client.update_metadata( - dataset, - metadata: {'maintainer_email': 'sample@datopian.com'} -) -``` - -For details of metadata see the [metadata reference below](#metadata-reference). - -## API - Porcelain - -### `Client.create` - -Expects as a single argument: a _string_, or a _dict_ (in Python), or an _object_ (in JavaScript). This argument is either a valid dataset name or dictionary with metadata for the dataset in Frictionless format. - -### `Client.push` - -Expects a single argument: a _dict_ (in Python) or an _object_ (in JavaScript) with a dataset metadata in Frictionless format. - -### `Client.retrieve` - -Expects a single argument: a string with a dataset name or uniquer ID. Returns a Frictionless resource as a _dict_ (in Python) or as an _Promisse .<object>_ (in JavaScript). - -### `Client.push_blob` - -Expects a single argument: a _dict_ (in Python) or an _object_ (in JavaScript) with a Frictionless resource. - -## API - Plumbing - -### `Client.action` - -This method bridges access to the CKAN API _action endpoint_. - -#### In Python - -Arguments: - -| Name | Type | Default | Description | -| -------------------- | ---------- | ---------- | ------------------------------------------------------------ | -| `name` | `str` | (required) | The action name, for example, `site_read`, `package_show`… | -| `payload` | `dict` | (required) | The payload being sent to CKAN. When a payload is provided to a GET request, it will be converted to URL parameters and each key will be converted to snake case. | -| `http_get` | `bool` | `False` | Optional, if `True` will make `GET` request, otherwise `POST`. | -| `transform_payload` | `function` | `None` | Function to mutate the `payload` before making the request (useful to convert to and from CKAN and Frictionless formats). | -| `transform_response` | `function` | `None` | function to mutate the response data before returning it (useful to convert to and from CKAN and Frictionless formats). | - ->[!note]The CKAN API uses the CKAN dataset and resource formats (rather than Frictionless formats). -In other words, to stick to Frictionless formats, you can pass `frictionless_ckan_mapper.frictionless_to_ckan` as `transform_payload`, and `frictionless_ckan_mapper.ckan_to_frictionless` as `transform_response`. - - -#### In JavaScript - -Arguments: - -| Name | Type | Default | Description | -| ------------ | ------------------- | ------------------ | ------------------------------------------------------------ | -| `actionName` | string | (required) | The action name, for example, `site_read`, `package_show`… | -| `payload` | object | (required) | The payload being sent to CKAN. When a payload is provided to a GET request, it will be converted to URL parameters and each key will be converted to snake case. | -| `useHttpGet` | object | false | Optional, if `True` will make `GET` request, otherwise `POST`. | - ->[!note]The JavaScript implementation uses the CKAN dataset and resource formats (rather than Frictionless formats). -In other words, to stick to Frictionless formats, you need to convert from Frictionless to CKAN before calling `action` , and from CKAN to Frictionless after calling `action`. - -## Metadata reference - ->[!info]Your site may have custom metadata that differs from the example set below. - - -### Profile - -**(`string`)** Defaults to _data-resource_. - -The profile of this descriptor. - -Every Package and Resource descriptor has a profile. The default profile, if none is declared, is `data-package` for Package and `data-resource` for Resource. - -#### Examples - -- `{"profile":"tabular-data-package"}` - -- `{"profile":"http://example.com/my-profiles-json-schema.json"}` - -### Name - -**(`string`)** - -An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed. - -This is ideally a url-usable and human-readable name. Name `SHOULD` be invariant, meaning it `SHOULD NOT` change when its parent descriptor is updated. - -#### Example - -- `{"name":"my-nice-name"}` - -### Path - -A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs. - -The dereferenced value of each referenced data source in `path` `MUST` be commensurate with a native, dereferenced representation of the data the resource describes. For example, in a *Tabular* Data Resource, this means that the dereferenced value of `path` `MUST` be an array. - -#### Validation - -##### It must satisfy one of these conditions - -###### Path - -**(`string`)** - -A fully qualified URL, or a POSIX file path.. - -Implementations need to negotiate the type of path provided, and dereference the data accordingly. - -**Examples** - -- `{"path":"file.csv"}` - -- `{"path":"http://example.com/file.csv"}` - -**(`array`)** - -**Examples** - -- `["file.csv"]` - -- `["http://example.com/file.csv"]` - -#### Examples - -- `{"path":["file.csv","file2.csv"]}` - -- `{"path":["http://example.com/file.csv","http://example.com/file2.csv"]}` - -- `{"path":"http://example.com/file.csv"}` - -### Data - -Inline data for this resource. - -### Schema - -**(`object`)** - -A schema for this resource. - -### Title - -**(`string`)** - -A human-readable title. - -#### Example - -- `{"title":"My Package Title"}` - -### Description - -**(`string`)** - -A text description. Markdown is encouraged. - -#### Example - -- `{"description":"# My Package description\nAll about my package."}` - -### Home Page - -**(`string`)** - -The home on the web that is related to this data package. - -#### Example - -- `{"homepage":"http://example.com/"}` - -### Sources - -**(`array`)** - -The raw sources for this resource. - -#### Example - -- `{"sources":[{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}]}` - -### Licenses - -**(`array`)** - -The license(s) under which the resource is published. - -This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein. - -#### Example - -- `{"licenses":[{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}]}` - -### Format - -**(`string`)** - -The file format of this resource. - -`csv`, `xls`, `json` are examples of common formats. - -#### Example - -- `{"format":"xls"}` - -### Media Type - -**(`string`)** - -The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml). - -#### Example - -- `{"mediatype":"text/csv"}` - -### Encoding - -**(`string`)** Defaults to _utf-8_. - -The file encoding of this resource. - -#### Example - -- `{"encoding":"utf-8"}` - -### Bytes - -**(`integer`)** - -The size of this resource in bytes. - -#### Example - -- `{"bytes":2082}` - -### Hash - -**(`string`)** - -The MD5 hash of this resource. Indicate other hashing algorithms with the {'{algorithm}'}:{'{hash}'} format. - -#### Examples - -- `{"hash":"d25c9c77f588f5dc32059d2da1136c02"}` - -- `{"hash":"SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0"}` - -## Generating templates - -You can use [`jsv`](https://github.com/datopian/jsv) to generate a template script in Python, JavaScript, and R. - -To install it: - -``` -$ npm install -g git+https://github.com/datopian/jsv.git -``` - -### Python - -``` -$ jsv data-resource.json --output py -``` - -**Output** -```python -dataset_metadata = { - "profile": "data-resource", # The profile of this descriptor. - # [example] "profile": "tabular-data-package" - # [example] "profile": "http://example.com/my-profiles-json-schema.json" - "name": "my-nice-name", # An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed. - "path": ["file.csv","file2.csv"], # A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs. - # [example] "path": ["http://example.com/file.csv","http://example.com/file2.csv"] - # [example] "path": "http://example.com/file.csv" - "data": None, # Inline data for this resource. - "schema": None, # A schema for this resource. - "title": "My Package Title", # A human-readable title. - "description": "# My Package description\nAll about my package.", # A text description. Markdown is encouraged. - "homepage": "http://example.com/", # The home on the web that is related to this data package. - "sources": [{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}], # The raw sources for this resource. - "licenses": [{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}], # The license(s) under which the resource is published. - "format": "xls", # The file format of this resource. - "mediatype": "text/csv", # The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml). - "encoding": "utf-8", # The file encoding of this resource. - # [example] "encoding": "utf-8" - "bytes": 2082, # The size of this resource in bytes. - "hash": "d25c9c77f588f5dc32059d2da1136c02", # The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format. - # [example] "hash": "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0" -} -``` - - -### JavaScript - -``` -$ jsv data-resource.json --output js -``` - -**Output** -```javascript -const datasetMetadata = { - // The profile of this descriptor. - profile: "data-resource", - // [example] profile: "tabular-data-package" - // [example] profile: "http://example.com/my-profiles-json-schema.json" - // An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed. - name: "my-nice-name", - // A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs. - path: ["file.csv", "file2.csv"], - // [example] path: ["http://example.com/file.csv","http://example.com/file2.csv"] - // [example] path: "http://example.com/file.csv" - // Inline data for this resource. - data: null, - // A schema for this resource. - schema: null, - // A human-readable title. - title: "My Package Title", - // A text description. Markdown is encouraged. - description: "# My Package description\nAll about my package.", - // The home on the web that is related to this data package. - homepage: "http://example.com/", - // The raw sources for this resource. - sources: [ - { - title: "World Bank and OECD", - path: "http://data.worldbank.org/indicator/NY.GDP.MKTP.CD", - }, - ], - // The license(s) under which the resource is published. - licenses: [ - { - name: "odc-pddl-1.0", - path: "http://opendatacommons.org/licenses/pddl/", - title: "Open Data Commons Public Domain Dedication and License v1.0", - }, - ], - // The file format of this resource. - format: "xls", - // The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml). - mediatype: "text/csv", - // The file encoding of this resource. - encoding: "utf-8", - // [example] encoding: "utf-8" - // The size of this resource in bytes. - bytes: 2082, - // The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format. - hash: "d25c9c77f588f5dc32059d2da1136c02", - // [example] hash: "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0" -}; -``` - -### R - -``` -$ jsv data-resource.json --output r -``` - -**Output** -```r -# The profile of this descriptor. -profile <- "data-resource" -# [example] profile <- "tabular-data-package" -# [example] profile <- "http://example.com/my-profiles-json-schema.json" -# An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed. -name <- "my-nice-name" -# A reference to the data for this resource, as either a path as a string, or an array of paths as strings. of valid URIs. -path <- ["file.csv","file2.csv"] -# [example] path <- ["http://example.com/file.csv","http://example.com/file2.csv"] -# [example] path <- "http://example.com/file.csv" -# Inline data for this resource. -data <- NA -# A schema for this resource. -schema <- NA -# A human-readable title. -title <- "My Package Title" -# A text description. Markdown is encouraged. -description <- "# My Package description\nAll about my package." -# The home on the web that is related to this data package. -homepage <- "http://example.com/" -# The raw sources for this resource. -sources <- [{"title":"World Bank and OECD","path":"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD"}] -# The license(s) under which the resource is published. -licenses <- [{"name":"odc-pddl-1.0","path":"http://opendatacommons.org/licenses/pddl/","title":"Open Data Commons Public Domain Dedication and License v1.0"}] -# The file format of this resource. -format <- "xls" -# The media type of this resource. Can be any valid media type listed with [IANA](https://www.iana.org/assignments/media-types/media-types.xhtml). -mediatype <- "text/csv" -# The file encoding of this resource. -encoding <- "utf-8" -# [example] encoding <- "utf-8" -# The size of this resource in bytes. -bytes <- 2082L -# The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format. -hash <- "d25c9c77f588f5dc32059d2da1136c02" -# [example] hash <- "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0" - -``` - - -## Design Principles - -The client **should** use Frictionless formats by default for describing dataset and resource objects passed to client methods. - -In addition, where more than metadata is needed (e.g., we need to access the data stream, or get the schema) we expect the _Dataset_ and _Resource_ objects to follow the [Frictionless Data Lib pattern](https://github.com/frictionlessdata/project/blob/master/rfcs/0004-frictionless-data-lib-pattern.md). diff --git a/site/content/docs/dms/ckan-enterprise/index.md b/site/content/docs/dms/ckan-enterprise/index.md deleted file mode 100644 index b44f9d5d..00000000 --- a/site/content/docs/dms/ckan-enterprise/index.md +++ /dev/null @@ -1,108 +0,0 @@ -# CKAN Enterprise - -## Introduction - -CKAN Enterprise is our name for what we plan would become our standard "base" distribution for CKAN going forward: - -* It is a CKAN standard code base with micro-services. -* Enterprise grade data catalog and portal targeted at Gov (open data portals) and Enterprise (Data Catalogs +). -* It is also known as [Datopian DMS](https://www.datopian.com/datopian-dms/). - -## Roadmap 2021 and beyond - -| | Current | CKAN Enterprise | -|-------------------|--------------------------------------------------------------------------------------------|-----------------------------------------------------------------| -| Raw storage | Filestore | Giftless | -| Data Loader (db) | DataPusher extension | Aircan | -| Data Storage (db) | Postgres | Any database engine. By default, Postgres | -| Data API (read) | Built-in DataStore extension's API including SQL endpoint | GraphQL based standalone micro-service | -| Frontend (public) | Build-in frontend into CKAN Classic python app (some projects are using nodejs app) | PortalJS or nodejs app | -| Data Explorer | ReclineJS (some projects that uses nodejs app for frontend have React based Data Explorer) | GraphQL based Data Explorer | -| Auth | Traditional login/password + extendable with CKAN Classic extensions | SSO with default Google, Github, Facebook and Microsoft options | -| Permissions | CKAN Classic based permissions | Existing permissions exposed via JWT based authz API | - -## Timeline 2021 - -To develop a base distribution of CKAN Enterprise, we want to build a demo project with the features from the roadmap. This way we can: - -* understand its advantages/limitations; -* compare against other instances of CKAN; -* demonstrate for the potential clients. - -High level overview of the planned features with ETA: - -| Name | Description | Effort | ETA | -| ----------------------------- | ------------------------------------ | ------ | --- | -| [Init](#Init) | Select CKAN version and deploy to DX | xs | Q2 | -| [Blobstore](#Blobstore) | Integrate Giftless for raw storage | s | Q2 | -| [Versioning](#Versioning) | Develop/integrate new versioning sys | l | Q3 | -| [DataLoader](#DataLoader) | Develop/integrate Aircan | xl | Q3 | -| [Data API](#Data-API) | Integrate new Data API (read) | m | Q2 | -| [Frontend](#Frontend) | Build a theme using PortalJS | s | Q2 | -| [DataExplorer](#DataExplorer) | Integrate into PortalJS | s | Q2 | -| [Permissions](#Permissions) | Develop permissions in read frontend | m | Q4 | -| [Auth](#Auth) | Integrate | s | Q4 | - -### Init - -Initialize a new project for development of CKAN Enterprise. - -Tasks: - -* Boot project in Datopian-DX cluster -* Use CKAN v2.8.x (latest patch) or 2.9.x -* Don't setup DataPusher -* Namespace: `ckan-enterprise` -* Domain: `enterprise.ckan.datopian.com` - -### Blobstore - -See [blob storage](/docs/dms/blob-storage#ckan-v3) - -### Versioning - -See [versioning](/docs/dms/versioning#ckan-v3) - -### DataLoader - -See [DataLoader](/docs/dms/load) - -### Data API - -* Install new [Data API service](https://github.com/datopian/data-api) in the project -* Install Hasura service in the project -* Set it up to work with DB of CKAN Enterprise -* Read more about Data API [here](/docs/dms/data-api#read-api-3) - -Notes: - -* We could experiment and use various features of Hasura, eg: - * Setting up row/column limits per user role (permissions) - * Subscriptions to auto load new data rows - -### Frontend - -PortalJS for the read frontend of CKAN Enterprise. [Read more](/docs/dms/frontend/#frontend). - -### DataExplorer - -A new Data Explorer based on GraphQL API: https://github.com/datopian/data-explorer-graphql - -### Permissions - -See [permissions](/docs/dms/permissions#permissions-authorization). - -### Auth - -Next generation, Kratos based, authentication (mostly SSO with no Traditional login by default) with following options out of the box: - -* GitHub -* Google -* Facebook -* Microsoft - -Easy to add: - -* Discord -* GitLab -* Slack diff --git a/site/content/docs/dms/ckan-v3/index.md b/site/content/docs/dms/ckan-v3/index.md deleted file mode 100644 index d161aa49..00000000 --- a/site/content/docs/dms/ckan-v3/index.md +++ /dev/null @@ -1,365 +0,0 @@ -# CKAN v3 - -## Introduction - -This document describes the architectures of CKAN v2 ("CKAN Classic"), CKAN v3 (also known as "CKAN Next Gen" for Next Generation), and CKAN v3 hybrid. The latter is an intermediate approach towards v3, where we still use CKAN v2 and common extensions, and only create microservices for new features. - -You will also find out how to do common tasks such as theming or testing, in each of the architectures. - -*Note: this blog post has an overview of the more decoupled, microservices approach at the core of v3: https://www.datopian.com/2021/05/17/a-more-decoupled-ckan/* - -## CKAN v2, CKAN v3 and Why v3 - -In yellow, you see one single Python process: - -```mermaid -graph TB - subgraph ckanclassic["CKAN Classic"] - ckancore["Core"] - end -``` - -When you want to extend core functionality of CKAN v2 (Classic), you write a Python package that must be installed in CKAN. This way, the extension will also run in the same process as the core functionality. This is known as a monolithic architecture. - -```mermaid -graph TB - subgraph ckanclassic["CKAN Classic"] - ckancore["Core"] --> ckanext["CKAN Extension 1"] - end -``` - -When you start to add multiple features, through extensions, what you get is one single Python process running many non-related functionalities. - -```mermaid -graph TB - subgraph ckanclassic["CKAN Classic"] - ckancore["Core"] --> ckanext["CKAN Extension 1"] - ckancore --> ckanext2["CKAN Extension 2"] - ckancore --> ckanext3["CKAN Extension 3"] - ckancore --> ckanext4["CKAN Extension 4"] - ckancore --> ckanext5["CKAN Extension 5"] - end -``` - -This monolithic approach has advantages in terms of simplicity of development and deployment, especially when the system is small. However, as it grows in scale and scope, there are an increasing number of issues. - -In this approach, an optional extension has the ability to crash the whole CKAN instance. Every new feature must be written in the same language and framework (e.g. Python, leveraging Flask or Django). And, perhaps most fundamentally, the overall system is highly coupled, making it complex and hard to understand, debug, extend, and evolve. - -### Microservices and CKAN v3 - -The main way to address these problems while gaining extra benefits is to move to a microservices-based architecture. - -Thus, we recommend building the next version of CKAN – CKAN v3 – on a microservices approach. - -[!tip]CKAN v3 is sometimes also referred to as CKAN Next Gen(eration). - -With microservices, each piece of functionality runs in its own service and process. - -```mermaid -graph TB - subgraph ckanapi3["CKAN API 3"] - ckanapi31["API 3"] - end - - subgraph ckanapi2["CKAN API 2"] - ckanapi21["API 2"] - end - - subgraph ckanapi1["CKAN API 1"] - ckanapi11["API 1"] - end - - subgraph ckanfrontend["CKAN frontend"] - ckanfrontend1["Frontend"] - end - - ckanfrontend1 --> ckanapi11 - ckanfrontend1 --> ckanapi21 - ckanfrontend1 --> ckanapi31 -``` - -### Incremental Evolution – Hybrid v3 - -One of the other advantages of the microservices approach is that it can also be used to extend and evolve current CKAN v2 solutions in an incremental way. We term these kinds of solutions "Hybrid v3," as they are a mix of v2 and v3 together. - -For example, a Hybrid v3 data portal could use a new microservice written in Node for the frontend, and combine that with CKAN v2 (with v2 extensions). - -```mermaid -graph TB - subgraph ckanapi3["CKAN API 3"] - ckanapi31["API 3"] - end - - subgraph ckanapi2["CKAN API 2"] - ckanapi21["API 2"] - end - - subgraph ckanapi1["CKAN API 1"] - ckanapi11["API 1"] - end - - subgraph ckanfrontend["CKAN frontend"] - ckanfrontend1["Frontend"] - end - - subgraph ckanclassic["CKAN Classic"] - ckancore["Core"] --> ckanext["CKAN Extension 1"] - ckancore --> ckanext2["CKAN Extension 2"] - end - - ckanfrontend1 --> ckancore - ckanfrontend1 --> ckanapi11 - ckanfrontend1 --> ckanapi21 - ckanfrontend1 --> ckanapi31 -``` - -The hybrid approach means we can evolve CKAN v2 "Classic" to CKAN v3 "Next Gen" incrementally. In particular, it allows people to keep using their existing v2 extensions, and upgrade them to new microservices gradually. - -### Comparison of Approaches - -| | CKAN v2 (Classic) | CKAN v3 (Next Gen) | CKAN v3 Hybrid | -| ------------ | ------------------| -------------------| ---------------| -| Architecture | Monolithic | Microservice | Microservice with v2 core | -| Language | Python | You can write services in any language you like.

Frontend default: JS.
Backend default: Python | Python and any language you like for microservices. | -| Frontend (and theming) | Python with Python CKAN extension | Flexible. Default is modern JS/NodeJS based | Can use old frontend but default to new JS-based frontend. | -| Data Packages | Add-on, no integration | Default internal and external format | Data Packages with converter to old CKAN format. | -| Extension | Extensions are libraries that are added to core runtime. They must therefore be built in python and are loaded into the core process at build time. "Template/inheritance" model where hooks are in core and it is core that loads and calls plugins. This means that if a hook does not exist in core then the extension is stymied. | Extensions are microservices and can be written in any language. They are loaded into the url space via kubernetes routing manager. Extensions hook into "core" via APIs (rather than in code). Follows a "composition" model rather than inheritance model | Can use old style extensions or microservices. | -| Resource Scaling | You have a single application so scaling is of the core application. | You can scale individual microservices as needed. | Mix of v2 and v3 | - -## Why v3: Long Version - -What are the problems with CKAN v2's monolithic architecture in relation to microservices v3? - -* **Poor Developer Experience (DX), innovability, and scalability due to coupling**. Monolithic means "one big system" => Coupling & Complexity => hard to understand, change and extend. Changes in one area can unexpectedly affect other areas. - * DX to develop a small new API requires wiring into CKAN core via an extension. Extensions can interact in unexpected ways. - * The core of people who fully understand CKAN has stayed small for a reason: there's a lot of understand. - * https://github.com/ckan/ckan/issues/5333 is an example of a small bug that's hard to track down due to various paths involved. - * Harder to make incremental changes due to coupling (e.g. Python 3 upgrade requires *everything* to be fixed at once - can't do rolling releases). -* **Stability**. One bad extension crashes or slows down the whole system -* **One language => Less developer flexibility (Poor DX)**. Have to write *everything* in Python, including the frontend. This is an issue especially for the frontend: almost all modern frontend development is heavily Javascript-based and theme is the #1 thing people want to customize in CKAN. At the moment, that requires installing *all* of CKAN core (using Docker) plus some familiarity with Python and Jinja templating. This is a big ask. -* **Extension stablity and testing**. Testing of extensions is painful (at least without careful factoring in a separate mini library) and are therefore often not tested; they don't have Continuous Integration (CI) or Continuous Deployment (CD). As an example, a highly experienced Python developer at Datopian was still struggling to get extension tests working 6 months into their CKAN work. -* **DX is poor especially when getting started**. Getting CKAN up and running requires multiple external services (database, Solr, Redis, etc.) making Docker the only viable way for bootstraping a local development environment. This makes getting started with CKAN daunting and painful. -* **Vertical scalability is poor**. Scaling the system is costly as you have to replicate the whole core process in every machine. -* **System is highly coupled.** Extensions b/c in process tend to end up with significant coupling to core which makes them brittle (has improved with plugins.toolkit) - * Upgrading core to Python 3 requires upgrading *all* extensions because they run in the same process. - * Search Index is not a separate API, but in Core. So replacing Solr is hard. - -The top 2 customizations of CKAN are slow and painful and require deep knowledge of CKAN: - -* Theming a site. -* Customizing the metadata. - -## Architectures - -### CKAN v2 (Classic) - -This diagram is based on the file `docker-compose.yml` of [github.com/okfn/docker-ckan](https://github.com/okfn/docker-ckan) (`docker-compose.dev.yml` has the same components, but different configuration). - -A difference from this diagram to the file is that we are not including DataPusher, as it is not a required dependency. - ->[!tip]Databases may run as Docker containers, or rely on third-party services such as Amazon Relational Database Service (RDS). - - - -```mermaid -graph LR - -CKAN[CKAN web app] - -CKAN --> DB[(Database)] -CKAN --> Solr[(Solr)] -CKAN --> Redis[(Redis)] - -subgraph Docker container - CKAN -end -``` - -Same setup showing some of the key extensions explicitly: - -```mermaid -graph LR - core[CKAN Core] --> DB[(Database)] - datastore --> DB2[(Database - DataStore)] - core --> Solr[(Solr)] - core --> Redis[(Redis)] - - subgraph Docker container - core - datastore - datapusher - imageview - ... - end -``` - -CKAN ships with several core extensions that are built-in. Here, together with the list of main components, we list a couple of them: - -Name | Type | Repository | Description ------|------|------------|------------ -CKAN | Application (API + Worker) | [Link](https://github.com/ckan/ckan) | Data management system (DMS) for powering data hubs and data portals. It's a monolithical web application that includes several built-in extensions and dependencies, such as a job queue service. In theory, it's possible to run it without any extensions. -datapusher | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/datapusher) | It could also be called "datapusher-connect." It's a glue code to connect with a separate microservice called DataPusher, which performs actions when new data arrives. -datastore | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/datastore) | The interface between CKAN and the structure database, the one receiving datasets and resources (CSVs). It includes an API for the database and an administrative UI. -imageview | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/imageview) | It provides an interface for creating HTML templates for image resources. -multilingual | CKAN Extension | [Link](https://github.com/ckan/ckan/tree/master/ckanext/multilingual) | It provides an interface for translation and localization. -Database | Database | | People tend to use a single PostgreSQL instance for this. Separated in multiple databases, it's the place where CKAN stores its own information (sometimes referred as "MetaStore" and "HubStore"), rows of resources (StructuredStore or DataStore), and raw datasets and resources ("BlobStore" or "FileStore"). The latter may store data in the local filesystem or cloud providers, via extensions. -Solr | Database | | It provides indexing and full-text search for CKAN. -Redis | Database | | Lightweight key-value store, used for caching and job queues. - -### CKAN v3 (Next Gen) - -CKAN Next Gen is still a DMS, as CKAN Classic; but rather than a monolithical architecture, it follows the microservices approach. CKAN Classic is not a dependency anymore, as we have smaller services providing functionality that we may or many not choose to include. This description is based on [Datopian's Technical Documentation](/docs/dms/ckan-v3/next-gen/#roadmap). - -```mermaid -graph LR - subgraph api3["..."] - api31["API"] - end - - subgraph api2["Administration"] - api21["API"] - end - - subgraph api1["Authentication"] - api11["API"] - end - - subgraph frontend["Frontend"] - frontendapi["API"] - end - - subgraph storage["Raw Resources Storage"] - storageapi["API"] - end - - storageapi --> cloudstorage[(Cloud Storage)] - - frontendapi --> storageapi - frontendapi --> api11 - frontendapi --> api21 - frontendapi --> api31 -``` - -At this moment, many important features are only available through CKAN extensions, so that brings us to the hybrid approach. - -### CKAN Hybrid v3 (Next Gen) - -We may sometimes make an explit distinction between CKAN v3 "hybrid" and "pure." The reason is because we want to ensure that we're not there yet – we have many opportunities to extract features out of CKAN and CKAN Extensions. - -In this approach, we still rely on CKAN Classic and all its extensions. Many already had many tests and bugs fixed, so we can deliver more if not forced to rewrite everything from scratch. - -```mermaid -graph TB - subgraph ckanapi3["CKAN API 3"] - ckanapi31["API 3"] - end - - subgraph ckanapi2["CKAN API 2"] - ckanapi21["API 2"] - end - - subgraph ckanapi1["CKAN API 1"] - ckanapi11["API 1"] - end - - subgraph ckanfrontend["Frontend"] - ckanfrontend1["Frontend v2"] - theme["[Project-specific theme]"] - end - - subgraph ckanclassic["CKAN Classic"] - ckancore["Core"] --> ckanext["CKAN Extension 1"] - ckancore --> ckanext2["[Project-specific extension]"] - end - - ckanfrontend1 --> ckancore - ckanfrontend1 --> ckanapi11 - ckanfrontend1 --> ckanapi21 - ckanfrontend1 --> ckanapi31 -``` - -Name | Type | Repository | Description ------|------|------------|------------ -Frontend v2 | Application | [Link](https://github.com/datopian/frontend-v2) | Node application for Data Portals. It communicates with a CKAN Classic instance, through its API, to get data and render HTML. It is written to be extensible, such as connecting to other applications and theming. -[Project-specific theme] | Frontend Theme | e.g., [Link](https://github.com/datopian/frontend-oddk) | Extension to Frontend v2 where you can personalize the interface, create different pages, and connect with other APIs. -[API 1] | Application | e.g., [Link](https://github.com/datopian/data-subscriptions) | Any application with an API to communicate with the user-facing Frontend v2 or to run tasks in background. Given the current architecture, often, this API is usually designed to work with CKAN interfaces. Over time, we may choose to make it more generic, and even replace CKAN Core with other applications. - -## Job Stories - -In this spreadsheet, you will find a list of common job stories in CKAN projects. Also, how you can accomplish them in CKAN v2, v3, and Hybrid v3. - -https://docs.google.com/spreadsheets/d/1cLK8xylprmVsoQIbdphqz9-ccSpdDABQExvKdvNJqaQ/edit#gid=757361856 - -## Glossary - -### API - -An HTTP API, usually following the REST style. - -### Application - -A Python package, an API, a worker... It may have other applications as dependencies. - -### CKAN Extension - -A Python package following specification from [CKAN Extending guide](https://docs.ckan.org/en/2.8/extensions/index.html). - -### Database - -An organized collection of data. - -### Dataset - -A group of resources made to be distributed together. - -### Frontend Theme - -A Node project specializing behavior present in [Frontend v2](https://github.com/datopian/frontend-v2). - -### Resource - -A data blob. Common formats are CSV, JSON, and PDF. - -### System - -A group of applications and databases that work together to accomplish a set of tasks. - -### Worker - -An application that runs tasks in background. They may run recurrently according to a given schedule, or as soon as it's requested by another application. - -## Appendix - -### Architecture - CKAN v2 with DataPusher - -```mermaid -graph TB - subgraph DataPusher - datapusherapi["DataPusher API"] - datapusherworker["CKAN Service Provider"] - SQLite[(SQLite)] - end - - subgraph CKAN - core - datapusher[datapusher ext] - datastore - ... - end - - core[CKAN Core] --> datastore - datastore --> DB[(Database)] - datapusherapi --> core - datapusher --> datapusherapi -``` - -Name | Type | Repository | Description ------|------|------------|------------ -DataPusher | System | [Link](https://github.com/ckan/datapusher) | Microservice that parses data files and uploads them to the datastore. -DataPusher API | API | [Link](https://github.com/ckan/datapusher) | HTTP API written in Flask. It is called from the built-in `datapusher` CKAN extension whenever a resource is created (and has the right type). -CKAN Service Provider | Worker | [Link](https://github.com/ckan/ckan-service-provider) | Library for making web services that make functions available as synchronous or asynchronous jobs. -SQLite | Database | | Unknown use. Possibly a worker dependency. - -### Old Next Gen Page - -Prior to this page, we had one called "Next Gen." It has intersections with this article, although it focuses more on the benefits of microservices. For the time being, the page still exists in [/ckan-v3/next-gen](/docs/dms/ckan-v3/next-gen), although it may get merged with this one in the future. diff --git a/site/content/docs/dms/ckan-v3/next-gen.md b/site/content/docs/dms/ckan-v3/next-gen.md deleted file mode 100644 index c997a933..00000000 --- a/site/content/docs/dms/ckan-v3/next-gen.md +++ /dev/null @@ -1,203 +0,0 @@ -# Next Gen - -“Next Gen” (NG) is our name for the evolution of CKAN from its current state as “CKAN Classic”. - -Next Gen has a decoupled, microservice architecture in contrast to CKAN Classic's monolithic architecture. It is also built from the ground up on the Frictionless Data principles and specifications which provide a simple, well-defined and widely adopted set of core interfaces and tooling for managing data. - -## Classic to Next Gen - -CKAN classic: monolithic architecture -- everything is one big python application. Extension is done at code level and "compiled in" at compile/run-time (i.e. you end up with one big docker file). - -CKAN Next Gen: decoupled, service-oriented -- services connected by network calls. Extension is done by adding new services, - -```mermaid -graph LR - -subgraph "CKAN Classic" - plugins -end - -subgraph "CKAN Next Gen" - microservices -end - -plugins --> microservices -``` - -You can read more about monolithic vs microservice architectures in the [Appendix below](#appendix-monolithic-vs-microservice-architecture). - - -## Next Gen lays the foundation for the future and brings major immediate benefits - -Next Gen's new approach is important in several major ways. - -### Microservices are the Future - -First, decoupled microservices have become *the* way to design and deploy (web) applications after first being pioneered by the likes of Amazon in the early 2000s. And in the last five to ten years have brought microservices "for the masses" with relevant tooling and technology standardized, open-sourced and widely deployed -- not only with containerization such as Docker, Kubernetes but also in programming languages like (server-side) Javascript and Golang. - -By adopting a microservice approach CKAN can reap the the benefits of what is becoming a mature and standard way to design and build (web) applications. This includes the immediate advantages of being aligned with the technical paradigm such as tooling and developer familiarity. - -### Microservices bring Scalability, Reliability, Extensibility and Flexibility - -In addition, and even more importantly, the microservices approach brings major benefits in: - -1. **Scalability**: dramatically easier and cheaper to scale up -- and down -- in size *and* complexity. Size-wise this is because you can replicate individual services rather than the whole application. Complexity-wise this is because monolithic architectures tend to become "big" where service-oriented encourages smaller lightweight components with cleaner interfaces. This means you can have a much smaller core making it easier to install, setup and extend. It also means you can use what you need making solutions easier to maintain and upgrade. -2. **Reliability**: easier (and cheaper) to build highly reliable, high availability solutions because microservices make isolation and replication easier. For example, in a microservice architecture a problem in CKAN's harvester won't impact your main portal because they run in separate containers. Similarly, you can scale the harvester system separately from the web frontend. -3. **Extensibility**: much easier to create and maintain extensions because they are a decoupled service and interfaces are leaner and cleaner. -4. **Flexibility** aka "Bring your own tech": services can be written in any language so, for example, you can write your frontend in javascript and your backend in Python. In a monolithic architecture all parts must be written in the same language because everything is compiled together. This flexibility makes it easier to use the best tool for the job. It also makes it much easier for teams to collaborate and cooperate and fewer bottlenecks in development. - -ASIDE: decoupled microservices reflect the "unix" way of building networked applications. As with the "unix way" in general, whilst this approach better -- and simpler -- in the long-run, in the short-run it often needs sustantial foundational work (those Unix authors were legends!). It may also be, at least initially, more resource intensive and more complex infrastructurally. Thus, whilst this approach is "better" it was not suprising that it was initially used for for complex and/or high end applications e.g. Amazon. This also explains why it took a while for this approach to get adoption -- it is only in the last few year that we have robust, lightweight, easy to use tooling and patterns for microservices -- "microservices for the masses" if you like. - -In summary, the Next Gen approach provides an essential foundation for the continuing growth and evolution of CKAN as a platform for building world-class data portal and data management solutions. - -## Evolution not Revolution: Next Gen Components Work with CKAN Classic - -*Gradual evolution from CKAN classic (keep what is working, keep your investments, incremental change)* - -Next Gen components are specifically designed to work with CKAN "Classic" in its current form. This means existing CKAN users can immediately benefit from Next Gen components and features whilst retaining the value of their existing investment. New (or existing) CKAN-based solutions can adopt a "hybrid" approach using components from both Classic and Next Gen. It also means that the owner of a CKAN-based solution can incrementally evolve from "Classic" to "Next Gen" by replacing one component one at a time, gaining new functionality without sacrificing existing work. - -ASIDE: we're fortunate that CKAN Classic itself was ahead of its time in its level of "service-orientation". From the start, it had a very rich and robust API and it has continued to develop this with almost almost all functionality exposed via the API. It is this rich API and well factored design that makes it relatively straightforward to evolve CKAN in its current "Classic" form towards Next Gen. - -## New Features plus Existing Functionality Improved - -In addition to its architecture, Next Gen provides a variety of improvements and extensions to CKAN Classic's functionality. For example: - -* Theming and Frontend Customization: theming and customizing CKAN's frontend has got radically easier and quicker. See [Frontend section »][frontend] -* DMS + CMS unified: integrate the full power of a modern CMS into your data portal and have one unified interface for data and content. See [Frontend section »][frontend] -* Data Explorer: the existing CKAN data preview/explorer has been completely rewritten in modern React-based Javascript (ReclineJS is now 7y old!). See [Data Explorer section »][explorer] -* Dashboards: build rich data-driven dashboards and integrate. See [Dashboards section »][dashboards] -* Harvesting: simpler, more powerful harvesting built on modern ETL. See [Harvesting section »][harvesting] - -And each of these features is easily deployed into an existing CKAN solution! - -[frontend]: /docs/dms/frontend -[explorer]: /docs/dms/data-explorer -[dashboards]: /docs/dms/dashboards -[harvesting]: /docs/dms/harvesting - -## Roadmap - -The journey to Next Gen from Classic can proceed step by step -- it does not need to be a big bang. Like refurbishing and extending a house, we can add a room here or renovate a room there whilst continuing to live happily in the building (and benefitting as our new bathroom comes online, or we get a new conservatory!). - -Here's an overview of the journey to Next Gen and current implementation status. More granular information on particular features may sometimes be found on the individual feature page, for example for [Harvesting here](/docs/dms/harvesting#design). - -```mermaid -graph LR - -start[Start] -themefe[Read Frontend] -authfe[Authentication in FE] -authzfe[Authorization in FE] -previews[Previews] -explorer[Explorer] -permsserv[Permissions Service] -orgs[Organizations] - - -subgraph Start - start -end - -subgraph Frontend - start --> themefe - themefe --> authfe - authfe --> authzfe - themefe --> revisioningfe[Revision UI] -end - -subgraph Harvesting - start --> harvestetl[Harvesting ETL + Runner] - harvestetl --> harvestui[Harvest UI] -end - -subgraph "Admin UI" - managedataset[Manage Dataset] - manageorg[Manage Organization] - manageuser[Manage Users] - manageconfig[Manage Config] - - start --> managedataset - start --> manageorg - managedataset --> manageconfig -end - -subgraph "Backend (API)" - start --> permsserv - start --> revision[Backend Revisioning] -end - -datastore[DataStore] - -subgraph DataStore - start --> datastore - datastore --> dataload[Data Load] -end - -subgraph Explorer - themefe --> previews - previews --> explorer -end - -subgraph Organizations - start --> orgs -end - -subgraph Key - done[Done] - nearlydone[Nearly Done] - inprogress[In Progress] - next[Next Up] -end - -classDef done fill:#21bf73,stroke:#333,stroke-width:3px; -classDef nearlydone fill:lightgreen,stroke:#333,stroke-width:3px; -classDef inprogress fill:orange,stroke:#333,stroke-width:2px; -classDef next fill:pink,stroke:#333,stroke-width:1px; - -class done,themefe,previews,explorer,harvestetl done; -class nearlydone,authfe,harvestui nearlydone; -class inprogress,dataload inprogress; -class next,permsserv next; -``` - -## Appendix: Monolithic vs Microservice architecture - -Monolithic: Libraries or modules communicate via function calls (inside one big application) - -Microservices: Services communicate over a network - -The best introduction and definition of microservices comes from Martin Fowler https://martinfowler.com/microservices/ - -> Microservice architectures will use libraries, but their primary way of componentizing their own software is by breaking down into services. We define libraries as components that are linked into a program and called using in-memory function calls, while services are out-of-process components who communicate with a mechanism such as a web service request, or remote procedure call. https://martinfowler.com/articles/microservices.html - -### Monolithic - -```mermaid -graph TD - -subgraph "Monolithic - all inside" - a - b - c -end - -a --in-memory function all--> b -a --in-memory function all--> c -``` - -### Microservice - -```mermaid -graph TD -subgraph "A Container" - a -end -subgraph "B Container" - b -end -subgraph "C Container" - c -end -a -.network call.-> b -a -.network call.-> c -``` diff --git a/site/content/docs/dms/ckan.md b/site/content/docs/dms/ckan.md deleted file mode 100644 index e51eb94f..00000000 --- a/site/content/docs/dms/ckan.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar: auto ---- - -# CKAN Classic - -CKAN (Classic) already has great documentation at: https://docs.ckan.org/ - -This material is a complement to those docs as well as details of our particular setup. Here, among other things, you'll learn how to: - -* [Get Started with CKAN for Development -- install and run CKAN on your local machine](/docs/dms/ckan/getting-started) -* [Play around with a CKAN instance including importing and visualising data](/docs/dms/ckan/play-around) -* [Install Extensions](/docs/dms/ckan/install-extension) -* [Create Your Own Extension](/docs/dms/ckan/create-extension) -* [Client Guide](/docs/dms/ckan-client-guide) -* [FAQ](/docs/dms/ckan/faq) - -[start]: /docs/dms/ckan/getting-started -[play]: /docs/dms/ckan/play-around - -[CKAN]: https://ckan.org/ -[docs]: https://docs.ckan.org/ - diff --git a/site/content/docs/dms/ckan/create-extension.md b/site/content/docs/dms/ckan/create-extension.md deleted file mode 100644 index 6661ed9f..00000000 --- a/site/content/docs/dms/ckan/create-extension.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -sidebar: auto ---- - -# Introduction -A CKAN extension is a Python package that modifies or extends CKAN. Each extension contains one or more plugins that must be added to your CKAN config file to activate the extension’s features. - -## Creating and Installing extensions -1. Boot up your docker compose -``` -docker-compose -f docker-compose.dev.yml up -``` - - -2. To create an extension template using this docker composition execute: - -``` -docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-example_extension -o /srv/app/src_extensions" -``` - -This command will create an extension template in your local `./src` folder that is mounted inside the containers in the `/srv/app/src_extension` directory. Any extension cloned on the `src` folder will be installed in the CKAN container when booting up Docker Compose (`docker-compose up`). This includes installing any requirements listed in a `requirements.txt` (or `pip-requirements.txt`) file and running `python setup.py develop`. - - -3. Add the plugin to the `CKAN__PLUGINS` setting in your `.env` file. - -``` -CKAN__PLUGINS=stats text_view recline_view example_extension -``` - - -4. Restart your docker-compose: - -``` -# Shut down your instance with crtl+c and then run it again with: -docker-compose -f docker-compose.dev.yml up -``` -> [!tip]CKAN will be started running on the paster development server with the '--reload' option to watch changes in the extension files. - -You should see the following output in the console: - -``` -... -ckan-dev_1 | Installed /srv/app/src_extensions/ckanext-example_extension -... -``` - -## Edit the extension - -Let's edit a template to change the way CKAN is displayed to the user! - -1. First you will need write permissions to the extension folder since it was created by the user running docker. Replace `your_username` and execute the following command: - -> [!tip]You can find out your current username by typing 'echo $USER' in the terminal. - -``` -sudo chown -R : src/ckanext-example_extension -``` - -2. The previous comamand creates all the files and folder structure needed for our extension. Open `src/ckanext-example_extension/ckanext/example_extension/plugin.py` to see the main file of our extension that we will edit to add custom functionality: - -```python -import ckan.plugins as plugins -import ckan.plugins.toolkit as toolkit - - -class Example_ExtensionPlugin(plugins.SingletonPlugin): - plugins.implements(plugins.IConfigurer) - - # IConfigurer - - def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('fanstatic', 'example_theme') -``` - -3. We will create a custom Flask Blueprint to extend our CKAN instance with more endpoints. In order to create a new blueprint and add an endpoint we need to: - - Import Blueprint and render_template from the flask module. - - Create the functions that will be used as endpoints - - Implement the IBlueprint interface in our plugin and add the new endpoint. - -4. From flask import Blueprint and render_template, - -```python -import ckan.plugins as plugins -import ckan.plugins.toolkit as toolkit - -from flask import Blueprint, render_template - -class Example_ExtensionPlugin(plugins.SingletonPlugin): - plugins.implements(plugins.IConfigurer) - - # IConfigurer - - def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('fanstatic', 'example_extension') -``` - -5. Create a new function: hello_plugin -```python -import ckan.plugins as plugins -import ckan.plugins.toolkit as toolkit - -from flask import Blueprint, render_template - -def hello_plugin(): - u'''A simple view function''' - return u'Hello World, this is served from an extension' - -class Example_ExtensionPlugin(plugins.SingletonPlugin): - plugins.implements(plugins.IConfigurer) - - # IConfigurer - - def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('fanstatic', 'example_extension') -``` -6. Implement the IBlueprint interface in our plugin and add the new endpoint. - -```python -import ckan.plugins as plugins -import ckan.plugins.toolkit as toolkit - -from flask import Blueprint, render_template - -def hello_plugin(): - u'''A simple view function''' - return u'Hello World, this is served from an extension' - -class Example_ExtensionPlugin(plugins.SingletonPlugin): - plugins.implements(plugins.IConfigurer) - plugins.implements(plugins.IBlueprint) - - # IConfigurer - - def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('fanstatic', 'example_extension') - - # IBlueprint - - def get_blueprint(self): - u'''Return a Flask Blueprint object to be registered by the app.''' - # Create Blueprint for plugin - blueprint = Blueprint(self.name, self.__module__) - blueprint.template_folder = u'templates' - # Add plugin url rules to Blueprint object - blueprint.add_url_rule('/hello_plugin', '/hello_plugin', hello_plugin) - return blueprint - -``` - -6. Go back to the browser and navigate to http://ckan:5000/hello_plugin. You should see the value returned by our view! - -![New Blueprint output](https://i.imgur.com/AZjTDbN.png) - -Now that you have added a new view and endpoint to your plugin you are ready for the next step of the tutorial! You can also check the complete code of this plugin in the [ckan repository](https://github.com/ckan/ckan/tree/master/ckanext/example_flask_iblueprint). diff --git a/site/content/docs/dms/ckan/faq.md b/site/content/docs/dms/ckan/faq.md deleted file mode 100644 index 3cf78381..00000000 --- a/site/content/docs/dms/ckan/faq.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -sidebar: auto ---- - -# FAQ - -This page provides answers to some frequently asked questions. - -## How to create an extension template in my local machine - -You can use the `paster` command in the same way as a source install. To create an extension execute the following command: - -``` -docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-myext -o /srv/app/src_extensions" -``` - -This will create an extension template inside the container's folder `/srv/app/src_extensions` which is mapped to your local `src/` folder. - -Now you can navigate to your local folder `src/` and see the extension created by the previous command and open the project in your favorite IDE. - - -## How to separate that extension in a new git repository so I can have the independence to install it in other instances - -Crucial thing is to understand that extensions get their repositories on GitHub (or elsewhere). You can first create a repository for extension and later clone in `src/` or do opposite as following: - -* Create the Extension, for example: `ckanext-myext`. -``` -docker-compose -f docker-compose.dev.yml exec ckan-dev /bin/bash -c "paster --plugin=ckan create -t ckanext ckanext-myext -o /srv/app/src_extensions" -``` - -* Init your new git repository into the extension folder `src/ckanext-myext` -``` -cd src/ckanext-myext -git init -``` -* Configure remote/origin -``` -git remote add origin -``` -* Add your files and push the first commit -``` -git add . -git commit -m 'Initial Commit' -git push -``` - -**Note:** The `src/` folder is gitignored in `okfn/docker-ckan` repository, so initializing new git repositories inside is ok. - -## How to quickly refresh the changes in my extension into the dockerized environment so I can have quick feedback of my changes - -This docker-compose setup for dev environment is already configured so that it sets `debug=True` inside configuration file and auto reloads on python and templates related changes. You do not have to reload when making changes to HTML, javascript or configuration files - you just need to refresh the page in the browser. - -See the CKAN images section of the [repository documentation](https://github.com/okfn/docker-ckan#ckan-images) for more detail - -## How to run tests for my extension in the dockerized environment so I can have a quick test-development cycle - -We write and store unit tests inside the `ckanext/myext/tests` directory. To run unit tests you need to be running the `ckan-dev` service of this docker-compose setup. - -* Once running, in another terminal window run the test command: -``` -docker-compose -f docker-compose.dev.yml exec ckan-dev nosetests --ckan-dev --nologcapture --reset-db -s -v --with-pylons=/srv/app/src_extensions/ckanext-myext/test.ini /srv/app/src_extensions/ckanext-myext/ -``` - -You can also pass nosetest arguments to debug -``` ---ipdb --ipdb-failure -``` - -**Note:** Right now all tests will be run, it is not possible to choose a specific file or test. - -## How to debug my methods in the dockerized environment so I can have a better understanding of whats going on with my logic - -To run a container and be able to add a breakpoint with `pdb`, run the `ckan-dev` container with the `--service-ports` option: - -``` -docker-compose -f docker-compose.dev.yml run --service-ports ckan-dev -``` - -This will start a new container, displaying the standard output in your terminal. If you add a breakpoint in a source file in the `src` folder (`import pdb; pdb.set_trace()`) you will be able to inspect it in this terminal next time the code is executed. - -## How to debug core CKAN code - -Currently, this docker-compose setup doesn't allow us to debug core CKAN code since it lives inside the container. However, we can do some hacks so the container uses a local clone of the CKAN core hosted in our machine. To do it: - -- Create a new folder called `ckan_src` in this `docker-ckan` folder at the same level of the `src/` -- Clone ckan and checkout the version you want to debug/edit - -``` -git https://github.com/ckan/ckan/ ckan_src -cd ckan_src -git checkout ckan-2.8.3 -``` - -- Edit `docker-compose.dev.yml` and add an entry to ckan-dev's and ckan-worker-dev's volumes. This will allow the docker container to access the CKAN code hosted in our machine. - -``` - - ./ckan_src:/srv/app/ckan_src -``` - -- Create a script in `ckan/docker-entrypoint.d/z_install_ckan.sh` to install CKAN inside the container from the cloned repository (instead of the one installed in the Dockerfile) - -``` -#!/bin/bash -echo "*********************************************" -echo "overriding with ckan installation with ckan_src" -pip install -e /srv/app/ckan_src -echo "*********************************************" -``` - -That's it. This will install CKAN inside the container in development mode, from the shared folder. Now you can open the `ckan_src/` folder from your favorite IDE and start working on CKAN. diff --git a/site/content/docs/dms/ckan/getting-started.md b/site/content/docs/dms/ckan/getting-started.md deleted file mode 100644 index 08d33f11..00000000 --- a/site/content/docs/dms/ckan/getting-started.md +++ /dev/null @@ -1,77 +0,0 @@ -# CKAN: Getting Started for Development - -## Prerequisites - -CKAN has a rich tech stack so we have opted to standardize our instructions with Docker Compose, which will help you spin up every service in a few commands. - -If you already have Docker-compose, you are ready to go! - -If not, please, follow instructions on [how to install docker-compose](https://docs.docker.com/compose/install/). - -On Ubuntu you can run: - -``` -sudo apt-get update -sudo apt-get install docker-compose -``` - -## Cloning the repo - -``` -git clone https://github.com/okfn/docker-ckan -# or git clone git@github.com:okfn/docker-ckan.git -cd docker-ckan -``` - -## Booting CKAN - -Create a local environment file: - -``` -cp .env.example .env -``` - -Build and Run the instances: - -> [!tip]'docker-compose' must be run with 'sudo'. If you want to change this, you can follow the steps below. NOTE: The 'docker' group grants privileges equivalent to the 'root' user. - -Create the `docker` group: `sudo groupadd docker` - -Add your user to the `docker` group: `sudo usermod -aG docker $USER` - -Change the storage directory ownership from `root` to `ckan` by adding the commads below to the `ckan/Dockerfile.dev` - -``` -RUN mkdir -p /var/lib/ckan/storage/uploads -RUN chown -R ckan:ckan /var/lib/ckan/storage -``` - -At this point, you can log out and log back in for these changes to apply. You can also use the command `newgrp docker` to temporarily enable the new group for the current terminal session. - -``` -docker-compose -f docker-compose.dev.yml up --build -``` - -When you see this log message: - -![](https://i.imgur.com/WUIiNRt.png) - -You can navigate to `http://localhost:5000` - -![CKAN Home Page](https://i.imgur.com/T5LWo8A.png) - -and log in with the credentials that docker-compose setup created for you [user: `ckan_admin` password:`test1234`]. - ->[!tip]To learn key concepts about CKAN, including what it is and how it works, you can read the User Guide. -[CKAN User Guide](https://docs.ckan.org/en/2.8/user-guide.html). - - -## Next Steps - -[Play around with CKAN portal](/docs/dms/ckan/play-around). - -## Troubleshooting - -Login / Logout button breaks the experience: - -- Change the URL from `http://ckan:5000` to `http://localhost:5000`. A complete fix is described in the [Play around with CKAN portal](/docs/dms/ckan/play-around). (Your next step. ;)) diff --git a/site/content/docs/dms/ckan/install-extension.md b/site/content/docs/dms/ckan/install-extension.md deleted file mode 100644 index 6a4c0edc..00000000 --- a/site/content/docs/dms/ckan/install-extension.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -sidebar: auto ---- - -# Installing extensions - -A CKAN extension is a Python package that modifies or extends CKAN. Each extension contains one or more plugins that must be added to your CKAN config file to activate the extension’s features. - -In this sections we will teach you only how to install existing extensions. See [next steps](/docs/dms/ckan/create-extension) in case you need to create or modify extensions - -## Add new extension - -Lets install [Hello World](https://github.com/rclark/ckanext-helloworld) on the portal. For that we need to do 2 thing: - -1. Install extension when building docker image -2. Add new extension to CKAN plugins - -### Install extension on docker build - -For this we need to modify Dockerfile for ckan service. Let's edit it: - -``` -vi ckan/Dockerfile.dev - -# Add following -RUN pip install -e git+https://github.com/rclark/ckanext-helloworld.git#egg=ckanext-helloworld -``` - -*Note:* In this example we use vi editor, but you can choose any of your choice. - -### Add new extension to plugins - -We need to modify .env file for that - Search for `CKAN_PLUGINS` and add new extension to the existing list: - -``` -vi .env - -CKAN__PLUGINS=helloworld envvars image_view text_view recline_view datastore datapusher -``` - -## Check extension is installed - -After modifying configuration files you will need to restart the portal. If your CKAN protal is up and running bring it down and re-start - -``` -docker-compose -f docker-compose.dev.yml stop -docker-compose -f docker-compose.dev.yml up --build -``` - -### Check what extensions you already have: - -http://ckan:5000/api/3/action/status_show - -Response should include list of all extensions including `helloworld` in it. - -``` -"extensions": [ - "envvars", - "helloworld", - "image_view", - "text_view", - "recline_view", - "datastore", - "datapusher" -] -``` - -### Check the extension is actually working - -This extension simply adds new route `/hello/world/name` to the base ckan and says hello - -http://ckan:5000/hello/world/John-Doe - -## Next steps - -[Create your own extension](/docs/dms/ckan/create-extension) diff --git a/site/content/docs/dms/ckan/play-around.md b/site/content/docs/dms/ckan/play-around.md deleted file mode 100644 index 9cf3003f..00000000 --- a/site/content/docs/dms/ckan/play-around.md +++ /dev/null @@ -1,285 +0,0 @@ ---- -sidebar: auto ---- - -# How to play around with CKAN - -In this section, we are going to show some basic functionality of CKAN focused on the API. - -## Prerequisites - -- We assume you've already completed the [Getting Started Guide](/docs/dms/ckan/getting-started). -- You have a basic understanding of Key data portal concepts: - -CKAN is a tool for making data portals to manage and publish datasets. You can read about the key concepts such as Datasets and Organizations in the User Guide -- or you can just dive in and play around! - -https://docs.ckan.org/en/2.9/user-guide.html - ->[!tip] -Install a [JSON formatter plugin for Chrome](https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa?hl=en) or browser of your choice. - -If you are familiar with the command line tool `curl`, you can use that. - -In this tutorial, we will be using `curl`, but for most of the commands, you can paste a link in your browser. For POST commands, you can use [Postman](https://www.getpostman.com/) or [Google Chrome Plugin](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop). - - -## First steps - ->[!tip] -By default the portal is accessible on http://localhost:5000. Let's update your `/etc/hosts` to access it on http://ckan:5000: - -``` -vi /etc/hosts # You can use the editor of your choice -# add following -127.0.0.1 ckan -``` - - -At this point, you should be able to access the portal on http://ckan:5000. - -![CKAN Home Page](https://i.imgur.com/T5LWo8A.png) - -Let's add some fixtures to it. For software, a fixture is something used consistently (in this case, data for you to play around with). Run the following from your terminal (do NOT cut the previous docker process as this one depends on the already launched docker, run in another terminal): - -```sh -docker-compose -f docker-compose.dev.yml exec ckan-dev ckan seed basic -``` - -Optionally you can `exec` into a running container using - -```sh -docker exec -it [name of container] sh -``` - -and run the `ckan` command there -```sh -ckan seed basic -``` - -You should be able to see 2 new datasets on home page: - -![CKAN with data](https://i.imgur.com/BiSifyb.png) - -To get more details on ckan commands please visit [CKAN Commands Reference](https://docs.ckan.org/en/2.9/maintaining/cli.html#ckan-commands-reference). - -### Check CKAN API - -This tutorial focuses on the CKAN API as that is central to development work and requires more guidance. We also invite you to explore the user interface which you can do directly yourself by visiting http://ckan:5000/. - -#### Let's check the portal status - -Go to http://ckan:5000/api/3/action/status_show. - -You should see something like this: - -```json -{ - "help": "https://ckan:5000/api/3/action/help_show?name=status_show", - "success": true, - "result": { - "ckan_version": "2.9.x", - "site_url": "https://ckan:5000", - "site_description": "Testing", - "site_title": "CKAN Demo", - "error_emails_to": null, - "locale_default": "en", - "extensions": [ - "envvars", - ... - "demo" - ] - } -} -``` - -This means everything is OK: the CKAN portal is up and running, the API is working as expected. In case you see an internal server error, please check the logs in your terminal. - -### A Few useful API endpoints to start with - -CKAN's Action API is a powerful, RPC-style API that exposes all of CKAN's core features to API clients. All of a CKAN website's core functionality (everything you can do with the web interface and more) can be used by external code that calls the CKAN API. - -#### Get a list of all datasets on the portal - -http://ckan:5000/api/3/action/package_list - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=package_list", - "success": true, - "result": ["annakarenina", "warandpeace"] -} -``` - -#### Search for a dataset - -http://ckan:5000/api/3/action/package_search?q=russian - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=package_search", - "success": true, - "result": { - "count": 2, - ... - } -} -``` - -#### Get dataset details - -http://ckan:5000/api/3/action/package_show?id=annakarenina - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=package_show", - "success": true, - "result": { - "license_title": "Other (Open)", - ... - } -} -``` - -#### Search for a resource - -http://ckan:5000/api/3/action/resource_search?query=format:plain%20text - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=resource_search", - "success": true, - "result": { - "count": 1, - "results": [ - { - "mimetype": null, - ... - } - ] - } -} -``` - -#### Get resource details - -http://ckan:5000/api/3/action/resource_show?id=288455e8-c09c-4360-b73a-8b55378c474a - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=resource_show", - "success": true, - "result": { - "mimetype": null, - ... - } -} -``` - -*Note:* These are only a few examples. You can find a full list of API actions in the [CKAN API guide](https://docs.ckan.org/en/2.9/api/#action-api-reference). - -### Create Organizations, Datasets and Resources - -There are 4 steps: - -- Get an API key; -- Create an organization; -- Create dataset inside an organization (you can't create a dataset without a parent organization); -- And add resources to the dataset. - -#### Get a Sysadmin Key - -To create your first dataset, you need an API key. - -You can see sysadmin credentials in the file `.env`. By default, they should be - -- Username: `ckan_admin` -- Password: `test1234` - -1. Navigate to http://ckan:5000/user/login and login. -2. Click on your username (`ckan_admin`) in the upright corner. -3. Scroll down until you see `API Key` on the left side of the screen and copy its value. It should look similar to `c7325sd4-7sj3-543a-90df-kfifsdk335`. - -#### Create Organization - -You can create an organization from the browser easily, but let's use [CKAN API](https://docs.ckan.org/en/2.9/api/#ckan.logic.action.create.organization_create) to do so. - -```sh -curl -X POST http://ckan:5000/api/3/action/organization_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{ - "name": "demo-organization", - "title": "Demo Organization", - "description": "This is my awesome organization" -}' -``` - -Response: - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=organization_create", - "success": true, - "result": {"users": [ - { - "email_hash": - ... - } - ]} -} -``` - -#### Create Dataset - -Now, we are ready to create our first dataset. - -```sh -curl -X POST http://ckan:5000/api/3/action/package_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{ - "name": "my-first-dataset", - "title": "My First Dataset", - "description": "This is my first dataset!", - "owner_org": "demo-organization" -}' -``` - -Response: - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=package_create", - "success": true, - "result": { - "license_title": null, - ... - } -} -``` - -This will create an empty (draft) dataset. - -#### Add a resource to it - -```sh -curl -X POST http://ckan:5000/api/3/action/resource_create -H "Authorization: 9c04a69d-79f4-4b4b-b4e1-f2ac31ed961c" -d '{ - "package_id": "my-first-dataset", - "url": "https://raw.githubusercontent.com/frictionlessdata/test-data/master/files/csv/100kb.csv", - "description": "This is the best resource ever!" , - "name": "brand-new-resource" -}' -``` - -Response: - -```json -{ - "help": "http://ckan:5000/api/3/action/help_show?name=resource_create", - "success": true, - "result": { - "cache_last_updated": null, - ... - } -} -``` - -That's it! Now you should be able to see your dataset on the portal at http://ckan:5000/dataset/my-first-dataset. - -## Next steps - -* [Install Extensions](/docs/dms/ckan/install-extension). diff --git a/site/content/docs/dms/cms-for-data-portals.md b/site/content/docs/dms/cms-for-data-portals.md deleted file mode 100644 index 62f1a6cc..00000000 --- a/site/content/docs/dms/cms-for-data-portals.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -sidebar: auto ---- - -# Content Management System (CMS) for Data Portals - -## Summary - -When selecting a CMS solution for Data Portals, we always recommend using headless CMS solution as it provides full flexibility when building your system. Headless CMS means only content (no HTML, CSS, JS) is created in the CMS backend and delivered to Frontend via API. - -> The traditional CMS approach to managing content put everything in one big bucket — content, images, HTML, CSS. This made it impossible to reuse the content because it was commingled with code. Read more - https://www.contentful.com/r/knowledgebase/what-is-headless-cms/. - -## Features - -Core features: - -* Create and manage blog posts (or news), e.g., `/news/abcd` -* Create and manage static pages, e.g., `/about`, `/privacy` etc. - -Important features: - -* User management, e.g., ability to manage editors so that multiple users can edit content. -* User roles, e.g., ability to assign different roles for users so that we can have admins, editors, reviewers. -* Draft content, e.g., useful when working on content development for review/feedback loop. However, this is not essential if you have multiple environments. -* A syntax for writing content with text formatting, multi-level headings, links, images, videos, bullet points. For example, markdown. -* User-friendly interface (text editor) to write content. - -```mermaid -graph LR - -CMS -.-> Blog["Blog or news section"] -CMS -.-> IndBlog["Individual blog post"] -CMS -.-> About["About page content"] -CMS -.-> TC["Terms and conditions page content"] -CMS -.-> Privacy["Privacy policy"] -CMS -.-> Other["Other static pages"] -``` - -## Options - -Headless CMS options: - -* WordPress (headless option) -* Drupal (headless option) -* TinaCMS - https://tina.io/ -* Git-based CMS - custom soltion based on Git repository. -* Strapi - https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html -* Ghost - https://ghost.org/docs/ -* CKAN Pages (built-in CMS option) - https://github.com/ckan/ckanext-pages - -*Note, there are loads of CMS available both in open-source and proprietary software. We are only considering few of them in this article and our requirement is that we should be able to fetch content via API (headless CMS). Readers are welcome to add more options into the list.* - -Comparison criteria: - -* Self-hosting (note this isn't criteria for most of projects and using managed hosting is a better option sometimes) -* Free and open source -* Multi language posts (unnecessary if your portal is single language) - -Comparison: - -| Options | Hosting | Free | Multi language | -| -------- | -------- | -------- | -------------- | -| Drupal | Tedious | Yes | Not straigtforward| -| WordPress| Tedious | Yes | Terrible UX | -| TinaCMS | Medium | Yes | Limited | -| Git-based| Easy | Yes | Custom | -| Strapi | Medium | Yes | Simple | -| Ghost | Medium | Yes | Simple | -| CKAN Pages| Easy | Yes | ? | - - -## Conclusion and recommendation - -Final decision should be made based on the following items: - -* How often editors will create content? E.g., daily, weekly, monthly, occasionally. -* How much content you already have and need to migrate? -* How many content editors you are planning to have? What are their technical expertise? -* Is there any specific requirements, e.g., you must host in your cloud? - -By default, we would recommend considering options such as Strapi, TinaCMS and Git-based CMS. We can even start with simple CKAN's built-in Pages and only move to sophisticated CMS once it is required. \ No newline at end of file diff --git a/site/content/docs/dms/dashboards.md b/site/content/docs/dms/dashboards.md deleted file mode 100644 index 45f5b735..00000000 --- a/site/content/docs/dms/dashboards.md +++ /dev/null @@ -1,163 +0,0 @@ -# Dashboards - -## What you can do? - -* Describe vizualizations in JSON and create interactive widgets -* Customize dashboard layout using well-known HTML -* Style dashboard design with TailwindCSS utility classes -* Rapidly create basic charts using "simple" graphing specification -* Create advanced widgets by utilizing "vega" visualization grammar - -## How? - -To create a dashboard you need to have some basic knowledge of: - -* git -* JSON -* HTML - -Before proceeding further, make sure you have forked the dashboards repository - https://github.com/datopian/dashboards. - -### Create a directory for your dashboard - -In the root of the project, create a directory for your dashboard. Name of this directory is the name of your dashboard so make it short and meaningful. Here is some good examples: - -* population -* environment -* housing - -So that your dashboard will be available at https://domain.com/dashboards/your-dashboard-name. - -Note that your dashboard directory will contain 2 files: - -* `index.html` - [HTML template](#Set-up-your-layout) -* `config.json` - [configurations for widgets](#Configure-vizualizations) - -### Set up your layout - -You need to prepare HTML template for your dashboard. No need to create entire HTML page but only snippet that is needed to inject the widgets: - -```html -

My example dashboard

-
-
-``` - -In the example above, we've created 2 div elements that we can reference by id when configuring vizualizations. - -Note that you can add any HTML tags and make your layout stand out. In the next section we'll explain how you do some stylings. - -### Style it - -This step is optional but if you have a dashboard with lots of widgets and metadata, you might want to style it so it appears nicely: - -* Use TailwindCSS utility classes **(recommended)** - * Official docs - https://tailwindcss.com/ - * Cheat sheet - https://nerdcave.com/tailwind-cheat-sheet -* Add inline CSS - -Example of using TailwindCSS utility classes: - -```html -

My example dashboard

-
-
-``` - -### Configure vizualizations - -In your config file `config.json` you can describe your dashboard in the following way: - -```json -{ - "widgets": [], - "datasets": [] -} -``` - -* `widgets` - a list of objects where each object contains information about where a widget should be injected and how it should look like (see below for examples). -* `datasets` - a list of dataset URLs. - -Example of a minimal widget object: - -```json -{ - "elementId": "widget1", - "view": { - "resources": [ - { - "datasetId": "", - "name": "" - } - ], - "specType": "", - "spec": {} - } -} -``` - -where: - -* `elementId` - is "id" of the HTML tag you want to use as a container of your widget. See [how we defined it here](#Set-up-your-layout). -* `view` - descriptor of a vizualization (widget). - * `resources` - a list of resources needed for a widget and required manipulations (transformations). - * `datasetId` - the id (name) of the dataset from which the resource is extracted. - * `name` - name of the resource. - * `transform` - transformations required for a resource (optional). If you want to learn more about transforms: - * Filtering data and applying formula: https://datahub.io/examples/transform-examples-on-co2-fossil-global#readme - * Sampling: https://datahub.io/examples/example-sample-transform-on-currency-codes#readme - * Aggregating data: https://datahub.io/examples/transform-example-gdp-uk#readme - * `specType` - type of a widget, e.g., `simple`, `vega` or `figure`. - * `spec` - specification for selected widget type. See below for examples. - * `title`, `legend`, `footer` - these are optional metadata for a widget. All must be a string. - -#### Basic charts - -Simple graph spec is the easiest and quickest way to specify a vizualization. Using simple graph spec you can generate line and bar charts: - -https://frictionlessdata.io/specs/views/#simple-graph-spec - -#### Advanced vizualizations - -Please check this instructions to create advanced graphs via Vega specification: - -https://frictionlessdata.io/specs/views/#vega-spec - -#### Figure widget - -The figure widget is used to display a single value from a dataset. For example, you might want to show latest unemployment rate in your dashboard so that it indicates current status of your cities economy. See left-hand side widgets here - https://london.datahub.io/. - -A specification for the figure widget would have the following structure: - -``` -{ - "fieldName": "", - "suffix": "", - "prefix": "" -} -``` - -where "fieldName" attribute will be used to extract specific value from a row. The "suffix" and "prefix" attributes are optional strings that is used to surround a figure, e.g., you can prepend a percent sign to indicate the number's value. - -Note that the first row of the data is used which means you need to transform data to show the relevant value. See this example for details - https://github.com/datopian/dashboard-js/blob/master/example/script.js#L12-L22. - -#### Example - -Check out carbon emission per capita dashboard as an example of creating advanced vizualizations: - -https://github.com/datopian/dashboards/tree/master/co2-emission-by-nation - -## Share it with the world! - -To make your dashboard live on the data portal, you need to: - -1. Simply create a pull request -2. Wait until your work gets reviewed and merged into "master" branch. -3. Implement any requested changes in your work. -4. Done! Your dashboard is now available at https://domain.com/dashboards/your-dashboard-name - - -## Research - -* http://dashing.io/ - no longer maintained as of 2016 - * Replaced by https://smashing.github.io/ diff --git a/site/content/docs/dms/dashboards/hdx-dashboards-notes.md b/site/content/docs/dms/dashboards/hdx-dashboards-notes.md deleted file mode 100644 index e3d050af..00000000 --- a/site/content/docs/dms/dashboards/hdx-dashboards-notes.md +++ /dev/null @@ -1,358 +0,0 @@ -# HDX Technical Architecture for Quick Dashboards - -Notes from analysis and discussion in 2018. - -# Concepts - -* Bite (View): a description of an individual chart / map / fact and its data (source) - * bite (for Simon): title, desc, data (compiled), uniqueid, map join info - * view (Data Package views): title, desc, data sources (on parent data package), transforms, ... - * compiled view: title, desc, data (compiled) -* Data source: - * Single HXL file (Currently, Simon's approach requires that all the data is in a single table so there is always a single data source.) - * Data Package(s) -* Creator / Editor: creating and editing the dashboard (given the source datasets) -* Renderer: given dashboard config render the dashboard - -# Dashboard Creator - -```mermaid -graph LR - -datahxl[data+hxl] -layouts[Layout options] -dashboard["Dashboard (config)

(Layout, Data Sources, Selected Bites)"] -editor[Editor] -bites[Bites
potential charts, maps etc] - -datahxl --suggester--> bites -bites --> editor -layouts --> editor -editor --save--> dashboard -``` - - -## Bite generation - -```mermaid -graph LR - -data[data with hxl] --> inferbites(("Iterate Recipes
and see what
matches")) -inferbites --> possmatches[List of potential bites] -possmatches --no map info--> done[Bite finished] -possmatches --lat+lon--> done -possmatches --geo info--> maplink(("Check pcodes
and link
map server url")) -maplink -.-> fuzzy((Fuzzy Matcher)) -fuzzy --> done -maplink --> done -maplink --error--> nobite[No Bite] -``` - -## Extending to non-HXL data - -It is easy to extend this to non-HXL data by using base HXL types and inference e.g. - -``` -date => #date -geo => #geo+lon -geo => #geo+lat -string/category => #indicator -``` - -```mermaid -graph LR - -data[data + syntax] -datahxl[data+hxl] -layouts[layout options] -dashboard["Dashboard (config)"] -editor[Editor] -bites[Bites
potential charts, maps etc] - -data --infer--> datahxl -datahxl --suggester--> bites -bites --> editor -layouts --> editor -editor --save--> dashboard -``` - -# Dashboard Renderer - -Rendering the dashboard involves: - -```mermaid -graph LR - -bites[Compiled Bites/Views] -renderer["Renderer
(Layout + charting / mapping libs)"] -data[Data] - -subgraph Dashboard Config - bitesconf[Bites/Views Config] - layoutconf[Layout Config] -end - -bitecompiler[Bite/View Compiler] -bitecompiler --> bites - -bitesconf --> bitecompiler -data --> bitecompiler - -layoutconf --> renderer -bites --> renderer - -renderer --> dashboard[HTML Dashboard] -``` - - -## Compiled View generation - -See https://docs.datahub.io/developers/views/ - - ----- - -# Architecture Proposal - -* data loader library - * File: rows, fields (rows, columns) -* type inference (?) - * syntax: table schema infer - * semantics (not now) -* data transform library (include hxl support) -* suggester library -* renderer library - -Interfaces / Objects - -* File -* (Dataset) -* Transform -* Algorithm / Recipe -* Bite / View -* Ordered Set of Bites -* Dashboard - -## File (and Dataset) - -http://okfnlabs.org/blog/2018/02/15/design-pattern-for-a-core-data-library.html - -https://github.com/datahq/data.js - -File - rows - descriptor - schema - schema - -## Recipe - -```json= -{ - 'id':'chart0001', - 'type':'chart', - 'subType':'row', - 'ingredients':[{'name':'what','tags':['#activity-code-id','#sector']}], - 'criteria':['what > 4', 'what < 11'], - 'variables': ['what', 'count()'], - 'chart':'', - 'title':'Count of {1}', -'priority': 8, -} -``` - -## Bite / Compiled View - -```json= -{ - bite: array [...data for chart...], - id: string "...chart bite ID...", - priority: number, - subtype: string "...bite subtype - row, pie...", - title: string "...title of bite...", - type: string "...bite type...", - uniqueID: string "...unique ID combining bite and data structure", -} -``` - -=> - - - -## Dashboard - -```json= -{ - "title":"", - "subtext":"", - "filtersOn":true, - "filters":[], - "headlinefigures":0, - "headlinefigurecharts":[ - ], - "grid":"grid5", - "charts":[ - { - "data":"https://proxy.hxlstandard.org/data.json?filter01=append&append-dataset01-01=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1FLLwP6nxERjo1xLygV7dn7DVQwQf0_5tIdzrX31HjBA%2Fedit%23gid%3D0&filter02=select&select-query02-01=%23status%3DFunctional&url=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1R9zfMTk7SQB8VoEp4XK0xAWtlsQcHgEvYiswZsj9YA4%2Fedit%23gid%3D0", - "chartID":"" - }, - { - "data":"https://proxy.hxlstandard.org/data.json?filter01=append&append-dataset01-01=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1FLLwP6nxERjo1xLygV7dn7DVQwQf0_5tIdzrX31HjBA%2Fedit%23gid%3D0&filter02=select&select-query02-01=%23status%3DFunctional&url=https%3A%2F%2Fdocs.google.com%2Fspreadsheets%2Fd%2F1R9zfMTk7SQB8VoEp4XK0xAWtlsQcHgEvYiswZsj9YA4%2Fedit%23gid%3D0", - "chartID":"" - } - ] -} -``` - -``` -var config = { - layout: 2x2 // in city-indicators dashboard is handcrafted in layout - widgets: [ - { - elementId / data-id: ... - view: { - metadata: { title, sources: "World Bank"} - resources: rule for creating compiled list of resources. [ { datasetId: ..., resourceId: ..., transform: ...} ] - specType: - viewspec: - } - }, - { - - }, - ] - datasets: [ - list of data package urls ... - ] -} -``` - -Simon's example - -https://simonbjohnson.github.io/hdx-iom-dtm/ - -```javascript= -{ - // metadata for dashboard - "title":"IOM DTM Example", - "subtext":" ....", - "headlinefigures": 3, - "grid": "grid5", // user chosen layout for dashboard. Choice of 10 grids - "headlinefigurecharts": [ //widgets - headline widget - { - "data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv", - "chartID": "text0013/#country+name/1" // bite Id - // elementId: ... // implicit from order in grid ... - }, - { - "data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv", - "chartID": "text0012/#affected+hh+idps/5" - }, - { - "data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv", - "chartID":"text0012/#affected+idps+ind/6" - } - ], - "charts": [ // chart widgets - { - "data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv", - "chartID": "map0002/#adm1+code/4/#affected+idps+ind/6", - "scale":"log" // chart config ... - }, - { - "data": "https://beta.proxy.hxlstandard.org/data/1d0a79/download/africa-dtm-baseline-assessments-topline.csv", - "chartID": "chart0009/#country+name/1/#affected+idps+ind/6", - "sort":"descending" - } - ] -} -``` - -Algorithm - -1. Extract the data references to a common list of datasets and fetch them -2. You generate compiled data via hxl.js plus own code transforming to final data for charting etc - - ``` - function transformChart(rawSourceData (csv parsed), bite) => [ [ ...], [...]] - data for chart - - hxl.js - custom code - - function transformMap - - function transformText ... - ``` - - - https://github.com/SimonbJohnson/hxlbites.js - - https://github.com/SimonbJohnson/hxlbites.js/blob/master/hxlBites.js#L957 - - ``` - hb.reverse(bite) => compiled bite (see above) (data, chartConfig) - ``` - -3. generate dashboard html and compute element ids in actual page element ids computed from grid setup -4. Now have a final dashboard config - - - ``` - widgets: [ - { - data: [ [...], [...]] - widgetType: text, chart, map ... - elementId: // element to bind to ... - } - ] - ``` -5. Now use specific renderer libraries e.g. leaflet, plotly/chartist etc to render out into page - - https://github.com/SimonbJohnson/hxldash/blob/master/js/site.js#L294 - -### Notes - -"Source" version of dashboard with data uncompiled. - -Compiled version of dashboard with final data inline ... - -hxl.js takes an array of arrays ... and outputs array of arrays ... - -``` -{ - schema: [...] - data: [...] -} -``` - -# Renderer - -* Renderer for the dashboard -* Renderer for each widget - - -``` -function createChart(bite, elementId) => svg in bite -``` - -## Charts - -* Data Package View => svg/png etc - * plotly - * vega (d3) - * https://github.com/frictionlessdata/datapackage-render-js -* chartist -* react-charts - -## Map - -* Leaflet -* react-leaflet - -## Tables - -... - - - - diff --git a/site/content/docs/dms/data-api.md b/site/content/docs/dms/data-api.md deleted file mode 100644 index d948d02b..00000000 --- a/site/content/docs/dms/data-api.md +++ /dev/null @@ -1,270 +0,0 @@ -# Data APIs (and the DataStore) - -## Introduction - -A Data API provides *API* access to data stored in a [DMS][]. APIs provide granular, per record access to datasets and their component data files. They offer rich querying functionality to select the records you want, and, potentially, other functionality such as aggregation. Data APIs can also provide write access, though this has traditionally been rarer.[^rarer] - -Furthermore, much of the richer functionality of a DMS or Data Portal such as data visualization and exploration require API data access rather than bulk download. - -[DMS]: /docs/dms/dms - -[^rarer]: It is rarer because write access usually means a) the data for this dataset is a structured database rather than a data file (which is normally more expensive both in terms b) the Data Portal has now become the primary (or semi-primary) home of this dataset rather simply being the host of a dataset whose home and maintenance is elsewhere. - -### API vs Bulk Access - -Direct download of a whole data file is the default method of access for data in a DMS. API access complements this direct download in "bulk" approach. In some situations API access may be the primary access option (so-called "API first"). In other cases, structured storage and API read/write may be the *only* way the data is stored and there is no bulk storage -- for example, this would be a natural approach for time series data which is being rapidly updated e.g. every minute. - -*Fig 1: Contrasting Download and API based access* - -```bash -# simple direct file access. You download -https://my-data-portal.org/my-dataset/my-csv-file.csv - -# API based access. Find the first 5 records with 'awesome' -https://my-data-portal.org/data-api/my-dataset/my-csv-file-identifier?q=awesome&limit=5 -``` - -In addition, to differing volume of access, APIs often differ from bulk download in their data format: following web conventions data APIs usually return the data in a standard format such as JSON (and can also provide various other formats e.g. XML). By contrast, direct data access necessarily supplies the data in whatever data format it was created in. - -### Limitations of APIs - -Whilst Data APIs are in many ways more flexible than direct download they have disadvantages: - -* APIs are much more costly and complex to create and maintain than direct download -* API queries are slow and limited in size because they run in real-time in memory. Thus, for bulk access e.g. of the entire dataset direct download is much faster and more efficient (download a 1GB CSV directly is easy and takes seconds but attempting to do so via the API may crash the server and be very slow). - -{/* -TODO: do more to compare and contrast download vs API access (e.g. what each is good for, formats, etc) -*/} - - -### Why Data APIs? - -Data APIs underpin the following valuable functionality on the "read" side: - -* **Data (pre)viewing**: reliably and richly (e.g. with querying, mapping etc). This makes the data much more accessible to non-technical users. -* **Visualization and analytics**: rich visualization and analytics may need a data API (because they need easily to query and aggregate parts of dataset). -* **Rich Data Exploration**: when exploring the data you will want to explore through a dataset quickly only pulling parts of the data and drilling down further as needed. -* **(Thin) Client applications**: with a data API third party users of the portal can build apps on top of the portal data easily and quickly (and without having to host the data themselves) - -Corresponding job stories would be like: - -* When building a visualization I want to select only some part of a dataset that I need for my visualization so that I can load the data quickly and efficiently. -* When building a Data Explorer or Data Driven app I want to slice/dice/aggregate my data (without downloading it myself) so that I can display that in my explorer / app. - -On the write side they provide support for: - -* **Rapidly updating data e.g. timeseries**: if you are updating a dataset every minute or every second you want an append operation and don't want to store the whole file every update just to add a single record -* **Datasets stored as structured data by default** and which can therefore be updated in part, a few records at a time, rather than all at once (as with blob storage) - - -## Domain Model - -The functionality associated to the Data APIs can be divided in 6 areas: - -* **Descriptor**: metadata describing and specifying the API e.g. general metadata e.g. name, title, description, schema, and permissions -* **Manager** for creating and editing APIs. - * API: for creating and editing Data API's descriptors (which triggers creation of storage and service endpoint) - * UI: for doing this manually -* **Service** (read): web API for accessing structured data (i.e. per record) with querying etc. *When we simply say "Data API" this is usually what we are talking about* - * Custom API & Complex functions: e.g. aggregations, join - * Tracking & Analytics: rate-limiting etc - * Write API: usually secondary because of its limited performance vs bulk loading - * Bulk export of query results especially large ones (or even export of the whole dataset in the case where the data is stored directly in the DataStore rather than the FileStore). This is an increasingly important featurea lower priority but if required it is substantive feature to implement. -* **Data Loader**: bulk loading data into the system that powers the data API. **This is covered in a [separate Data Load page](/docs/dms/load/).** - * Bulk Load: bulk import of individual data files - * Maybe includes some ETL => this takes us more into data factory -* **Storage (Structured)**: the underlying structured store for the data (and its layout). For example, Postgres and its table structure.This could be considered a separate component that the Data API uses or as part of the Data API -- in some cases the store and API are completely wrapped together, e.g. ElasticSearch is both a store and a rich Web API. - ->[!tip]Visualization is not part of the API but the demands of visualization are important in designing the system. - -## Job Stories - -### Read API - -When I'm building a client application or extracting data I want to get data quickly and reliably via an API so that I can focus on building the app rather than manging the data - -* Performance: Querying data is **quick** -* Filtering: I want to filter data easily so that I can get the slice of data that I need. - * ❗ unlimited query size for downloading eg, can download filtered data with millions of rows -* can get results in 3 formats: CSV, JSON and Excel. -* API formats - * "Restful" API (?) - * SQL API (?) - * ❗ GraphQL API (?) - * ❗ custom views/cubes (including pivoting) -* Query UI - -:exclamation: = something not present atm - -#### Retrieve records via an API with filtering (per resource) (if tabular?) - -When I am building a web app, a rich viz, display the data, etc I want to have an API to data (returns e.g. JSON, CSV) [in a resource] so that I can get precise chunks of data to use without having to download and store the whole dataset myself - -* I want examples -* I want a playground interface … - -#### Bulk Export - -When I have a query with a large amount of results I want to be able to download all of those results so that I can analyse them with my own tools - -#### Multiple Formats - -When querying data via the API I want to be able to get the results in different formats (e.g. JSON, CSV, XML (?), ...) so that I can get it in a format most suitable for my client application or tool - -#### Aggregate data (perform ops) via an API … - -When querying data to use in a client application I want to be able to perform aggregations such as sum, group by etc so that I can get back summary data directly and efficiently (and don't have to compute myself or wait for large amounts of data) - -#### SQL API - -When querying the API as a Power User I want to use SQL so that I can do complex queries and operations and reuse my exisitng SQL knowledge - -#### GeoData API - -When querying a dataset with geo attributes such as location I want to be able use geo-oriented functionality e.g. find all items near X so that I can find the records I want by location - -#### Free Text Query (Google Style / ElasticSearch Style) - -When querying I want to do a google style search in data e.g. query for "brown" and find all rows with brown in them or do `brown road_name:*jfk*` and get all results with brown in them and whose field `road_name` has `jfk` in it so that I can provide a flexible query interface to my users - -#### Custom Data API - -As a Data Curator I want to create a custom API for one or more resources so that users can access my data in convenient ways … - -* E.g. query by dataset or resource name rather than id ... - -#### Search through all data (that is searchable) / Get Summary Info - -As a Consumer I want to search across all the data in the Data Portal at once so that I can find the value I want quickly and easily … (??) - -#### Search for variables used in datasets - -As a Consumer (researcher/student …) I want to look for datasets with particular variables in them so that I can quickly locate the data I want for my work - -* Search across the column names so that ?? - -#### Track Usage of my Data API - -As a DataSet Owner I want to know how much my Data API is being used so that I can report that to stakeholders / be proud of that - -#### Limit Usage of my Data API (and/or charge for it) - -As a Sysadmin I want to limit usage of my Data API per user (and maybe charge for above a certain level) so that I don’t spend too much money - -#### Restrict Access to my Data API - -As a Publisher I want to only allow specific people to access data via the data API so that … - -* Want this to mirror the same restrictions I have on the dataset / resources elsewhere (?) - -### UI for Exploring Data - ->[!warning]This probably is not a Data API epic -- rather it would come under the Data Explorer. - -* I want an interface to “sql style” query data -* I want a filter interface into data -* I want to download filtered data -* ... - -### Write API - -When adding data I want to write new rows via the data API so that the new data is available via the API - -* ? do we also want a way to do bulk additions? - - -### DataStore - -When creating a Data API I want a structured data store (e.g. relational database) so that I can power the Data API and have it be fast, efficient and reliable. - - -## CKAN v2 - -In CKAN 2 the bulk of this functionality is in the core extension `ckanext-datastore`: - -* https://docs.ckan.org/en/2.8/maintaining/datastore.html -* https://github.com/ckan/ckan/tree/master/ckanext/datastore - -In summary: the underlying storage is provided by a Postgres database. A dataset resource is mapped to a table in Postgres. There are no relations between tables (no foreign keys). A read and write API is provided by a thin Python wrapper around Postgres. Bulk data loading is provided in separate extensions. - -### Implementing the 4 Components - -Here's how CKAN 2 implements the four components described above: - -* Read API: is provided by an API wrapper around Postgres. This is written as a CKAN extension written in Python and runs in process in the CKAN instance. - * Offers both classic Web API query and SQL queries. - * Full text, cross field search is provided via Postgres and creating an index concatenating across fields. - * Also includes a write API and functions to create tables -* DataStore: a dedicated Postgres database (separate to the main CKAN database) with one table per resource. -* Data Load: provided by either DataPusher (default) or XLoader. More details below. - * Utilize the CKAN jobs system to load data out of band - * Some reporting integrated into UI - * Supports tabular data (CSV or Excel) : this converts CSV or Excel into data that can be loaded into the Postgres DB. -* Bulk Export: you can bulk download via the extension using the dump functionality https://docs.ckan.org/en/2.8/maintaining/datastore.html#downloading-resources - * Note however this will have problems with large resources either timing out or hanging the server - -### Read API - -The CKAN DataStore extension provides an ad-hoc database for storage of structured data from CKAN resources. - -See the DataStore extension: https://github.com/ckan/ckan/tree/master/ckanext/datastore - -[Datastore API](https://docs.ckan.org/en/2.8/maintaining/datastore.html#the-datastore-api) - -[Making Datastore API requests](https://docs.ckan.org/en/2.8/maintaining/datastore.html#making-a-datastore-api-request) - -[Example: Create a DataStore table](https://docs.ckan.org/en/2.8/maintaining/datastore.html#ckanext.datastore.logic.action.datastore_create) - -```sh -curl -X POST http://127.0.0.1:5000/api/3/action/datastore_create \ - -H "Authorization: {YOUR-API-KEY}" \ - -d '{ "resource": {"package_id": "{PACKAGE-ID}"}, "fields": [ {"id": "a"}, {"id": "b"} ] }' -``` - - -### Data Load - -See [Load page](/docs/dms/load#ckan-v2). - -### DataStore - -Implemented as a separate Postgres Database. - -https://docs.ckan.org/en/2.8/maintaining/datastore.html#setting-up-the-datastore - -### What Issues are there? - -Sharp Edges - -* connection between MetaStore (main CKAN objects DB) and DataStore is not always well maintained e.g, if I call “purge_dataset” action, it will remove stuff from MetaStore but it won’t delete a table from DataStore. This does not break UX but your DataStore DB raises in size and you might have junk tables with lots of data. - -DataStore (Data API) - -* One table per resource and no way to join across resources -* Indexes are auto-created and no way to customize per resource. This can lead to issues on loading large datasets. -* No API gateway (i.e. no way to control DDOS’ing, to do rate limiting etc) -* SQL queries not working (with private datasets) - -## CKAN v3 - -Following the general [next gen microservices approach][ng], the Data API is separated into distinct microservices. - -[ng]: /docs/dms/ckan-v3/next-gen - -### Read API - -Approach: Refactor current DataStore API into a standalone microservice. Key point would be to break out permissioning. Either via a call out to separate permissioning service or a simple JWT approach where capability is baked in. - -Status: In Progress (RFC) - see https://github.com/datopian/data-api - -### Data Load - -Implemented via AirCan. See [Load page](/docs/dms/load). - -### Storage - -Back onto Postgres by default just like CKAN 2. May also explore using other backends esp from Cloud Providers e.g. BigQuery or AWS RedShift etc. - -* See Data API service https://github.com/datopian/data-api -* BigQuery: https://github.com/datopian/ckanext-datastore-bigquery diff --git a/site/content/docs/dms/data-explorer.md b/site/content/docs/dms/data-explorer.md deleted file mode 100644 index 3e32fc30..00000000 --- a/site/content/docs/dms/data-explorer.md +++ /dev/null @@ -1,282 +0,0 @@ ---- -sidebar: auto ---- - -# Data Explorer - -The Datopian Data Explorer is a React single page application and framework for creating and displaying rich data explorers (think Tableau-lite). Use stand-alone or with CKAN. For CKAN it is a drop-in replacement for ReclineJS in CKAN Classic. - -![Data Explorer](/static/img/docs/dms/data-explorer/data-explorer.png) -> [Data Explorer for the City of Montreal](http://montreal.ckan.io/ville-de-montreal/geobase-double#resource-G%C3%83%C2%A9obase%20double) - -## Features / Highlights - -"Data Explorer" is an embedable React/Redux application that allows users to: - -* Explore tabular, map, PDF, and other types of data -* Create map views of tabular data using the [Map Builder](#map-builder) -* Create charts and graphs of tabular data using [Chart Builder](#chart-builder) -* Easily build SQL queries for Data Store API using graphical interface of [Datastore Query Builder](#datastore-query-builder) - -## Components - -The Data Explorer application acts as a coordinating layer and state management solution -- via [Redux](https://redux.js.org/) -- for several libraries, also maintained by Datopian. - -### [Datapackage Views](https://github.com/datopian/datapackage-views-js) - -![Datapackage Views](/static/img/docs/dms/data-explorer/datapackage-views.png) - -Datapackage View is the rendering engine for the main window of the Data Explorer. - -The above image displays a table shown at the `Table` tab, but note that Datapackage-views renders _all_ data visualizations: Tables, Charts, Maps, and others. - -### [Datastore Query Builder](https://github.com/datopian/datastore-query-builder) - -Datastore Query Builder - -The Datastore Query Builder interfaces with the Datastore API to allow users to search data resources using an SQL like interface. See the docs for this module here - [Datastore Query Builder docs](/docs/dms/data-explorer/datastore-query-builder/). - -### [Map Builder](https://github.com/datopian/map-builder) - -Map Builder - -Map Builder allows users to build maps based on geo-data contained in tabular resources. - -Supported geo formats: -* lon / lat (separate columns) - -### [Chart Builder](https://github.com/datopian/chart-builder) - -Chart Builder - -Chart Builder allows users to create charts and graphs from tabular data. - -## Quick-start (Sandbox) - -* Clone the data explorer -```bash -$ git clone git@gitlab.com:datopian/data-explorer.git -``` -* Use yarn to install the project dependencies -```bash -$ cd data-explorer -$ yarn -``` -* To see the Data Explorer running in a sandbox environment run [Cosmos](https://github.com/react-cosmos/react-cosmos) -```bash -$ yarn cosmos -``` - -## Configuration - -[`data-datapackage` attribute](#add-data-explorer-tags-to-the-page-markup) may influence how the element will be displayed. It can be created from a [datapackage descriptor](https://frictionlessdata.io/specs/data-package/). - -### Fixtures - -Until we have better documentation on Data Explorer settings, use the [Cosmos fixtures](https://gitlab.com/datopian/data-explorer/blob/master/__fixtures__/with_widgets/geojson_simple.js) as an example of how to instantiate / configure the Data Explorer. - -### Serialized state - -`store->serializedState` is a representation of the application state _without fetched data_ -A data-explorer can be "hydrated" using the serialized state, it will refetch the data, and will render in the same state it was exported in - -### Share links - -Share links can be added in `datapakage.resources[0].api` attribute. - -There is common limit of up 2000 characters on URL strings. Our share links contain the entire application store tree, which is often larger than 2000 characters, in which the application state cannot be shared via URL. Thems the breaks. - -## Translations - -### Add a Translation To Data Explorer - -To add a translation to a new language to the data explorer you need to: - -1. clone the repository you need to update - - ```bash - git clone git@gitlab.com:datopian/data-explorer.git - ``` -2. go to `src/i18n/locales/` folder -3. add a new sub-folder with locale name and the new language json file (e.g. `src/i18n/locales/ru/translation.json`) -4. add the new file to resources settings in `i18n.js`: -`src/i18n/i18n.js`: -```javascript -import en from './locales/en/translation.json' -import da from './locales/da/translation.json' -import ru from './locales/ru/translation.json' - ... - ru: { - translation: { - ...require('./locales/ru/translation.json'), - ... - } - }, - ... -``` -5. create a merge request with the changes - -### Add a translation To a Component - -Some strings may come from a component, to add translation for them will require some extra steps, e.g. datapackage-views-js: - -1. clone the repository - ```bash - https://github.com/datopian/datapackage-views-js.git - ``` -2. go to `src/i18n/locales/` folder -3. add a new sub-folder with locale name and the new language json file (e.g. `src/i18n/locales/ru/translation.json`) -4. add the new file to resources settings in `i18n.js`: -`src/i18n/i18n.js`: -```javascript -... -import ru from './locales/ru/translation.json' - ... - resources: { - ... - ru: {translation: ru}, - }, - ... -``` -5. create a pull request for datapackage-views-js -6. get the new datapackage-views-js version after merging (e.g. 1.3.0) -7. clone data-explorer -8. upgrade the data-explorer's datapackage-views-js dependency with the new version: - a. update package.json - b. run `yarn install` -9. add the component's translations path to Data Explorer: -```javascript -import en from './locales/en/translation.json' -import da from './locales/da/translation.json' -import ru from './locales/ru/translation.json' - ... - ru: { - translation: { - ...require('./locales/ru/translation.json'), - ...require('datapackage-views-js/src/i18n/locales/ru/translation.json'), - } - }, - ... -``` -10. create a merge request for data-explorer - -### Testing a Newly Added Language - -To see your language changes in Data Explorer you can run `yarn start` and change the language cookie of the page (`defaultLocale`): - -![i18n Cookie](/static/img/docs/dms/data-explorer/i18n-cookie.png) - -### Language detection - -Language detection rules are determined by `detection` option in `src/i18n/i18n.js` file. Please edit with care, as other projects may already depend on them. - -## Embedding in CKAN NG Theme - -### Copy bundle files to theme's `public` directory - -```bash -$ cp data-explorer/build/static/js/*.js frontend-v2/themes/your_theme/public/js -$ cp data-explorer/build/static/js/*.map frontend-v2/themes/your_theme/public/js -$ cp data-explorer/build/static/css/* frontend-v2/themes/your_theme/public/css -``` - - -#### Note on app bundles - -The bundled resources have a hash in the filename, for example `2.a3e71132.chunk.js` - -During development it may be preferable to remove the hash from the file name to avoid having to update the script tag during iteration, for example - -```bash -$ mv 2.a3e71132.chunk.js 2.chunk.js -``` - -A couple caveats: -* The `.map` file names should remain the same so that they are loaded properly -* Browser cache may need to be invalidated manually to ensure that the latest script is loaded - - -### Require Data Explorer resources in NG theme template - -In `/themes/your-theme/views/your-template-wth-explplorer.html` - -```html - -{% block content %} - - - - - -``` - -### Configure datapackage - -```htmlmixed= - - - const datapackage = { - resources: [{resource}], // single resource for this view - views: [...], // can be 3 views aka widgets - controls: { - showChartBuilder: true, - showMapBuilder: true - } - } - -``` - -### Add data-explorer tags to the page markup - -Each Data Explorer instance needs a corresponding `
` in the DOM. For example: - -```html -{% for resource in dataset.resources %} -
-{% endfor %} -``` - -Note that each container div needs the following attributes: -* `class="data-explorer"` (All explorer divs should have this class) -* `id="data-explorer-0"` (1, 2, etc...) -* `data-datapackage=`{'{JSON CONFIG}'}` (A valid JSON configuration) - -### Add data explorer scripts to your template - -```html - - - -``` - -*NOTE* that the scripts should be loaded _after the container divs are in the DOM, typically by placing the ` - - - -
- - Frequency Threshold
-

-  
-
-```
-
-`df` is a Dataflow instance where we register (.add) functions and parameters - as below on line 36-38. The same with adding transforms - lines 40-44. We can pass different parameters to the transforms depending on requirements of each of them. Event handlers can added by using `.on` method of the Dataflow instance - lines 46-48.
-
-```javascript
-var tx = vega.transforms; // all transforms 
-var out = document.querySelector('#output');
-var area = document.querySelector('#text');
-area.value = [
-  "Despite myriad tools for visualizing data, there remains a gap between the notational efficiency of high-level visualization systems and the expressiveness and accessibility of low-level graphical systems."
-].join('\n\n');
-var stopwords = "(i|me|my|myself|we|us|our|ours|ourselves|you|your|yours|yourself|yourselves|he|him|his)";
-
-var get = vega.field('data');
-
-function readText(_, pulse) {
-  if (this.value) pulse.rem = this.value;
-  return pulse.source = pulse.add = [vega.ingest(area.value)];
-}
-
-function threshold(_) {
-  var freq = _.freq,
-      f = function(t) { return t.count >= freq; };
-  return (f.fields = ['count'], f);
-}
-
-function updatePage() {
-  out.innerText = c1.value.slice()
-    .sort(function(a,b) {
-      return (b.count - a.count)
-        || (b.text > a.text ? -1 : a.text > b.text ? 1 : 0);
-    })
-    .map(function(t) {
-      return t.text + ': ' + t.count;
-    })
-    .join('\n');
-}
-
-var df = new vega.Dataflow(), // create a new Dataflow instance
-// then add various operators into Dataflow instance:
-    ft = df.add(4), // word frequency threshold
-    ff = df.add(threshold, {freq:ft})
-    rt = df.add(readText),
-    // add a transforms (tx):
-    cp = df.add(tx.CountPattern, {field:get, case:'lower',
-      pattern:'[\\w\']{2,}', stopwords:stopwords, pulse:rt}),
-    cc = df.add(tx.Collect, {pulse:cp}),
-    fc = df.add(tx.Filter, {expr:ff, pulse:cc}),
-    c1 = df.add(tx.Collect, {pulse:fc}),
-    up = df.add(updatePage, {pulse: c1});
-df.on(df.events(area, 'keyup').debounce(250), rt)
-  .on(df.events('#slider', 'input'), ft, function(_, e) { return +e.target.value; })
-  .run();
-```
-
----
-
-#### Old analysis
-
-There are number of transforms and they are located in different libraries. Basics are here https://github.com/vega/vega-dataflow/tree/master/src/transforms
-
-Generally, all data flow happens in the [vega-dataflow module](https://github.com/vega/vega-dataflow). There are lots of complicated operations performed to data input and parameters. Some of transform functions are inherited from another functions/classes which makes difficult to separate them:
-
-Filter function:
-
-```javascript
-export default function Filter(params) {
-  Transform.call(this, fastmap(), params);
-}
-
-var prototype = inherits(Filter, Transform);
-
-// more code for prototype
-```
-and Transform is:
-```javascript
-import Operator from './Operator';
-import {inherits} from 'vega-util';
-
-/**
- * Abstract class for operators that process data tuples.
- * Subclasses must provide a {@link transform} method for operator processing.
- * @constructor
- * @param {*} [init] - The initial value for this operator.
- * @param {object} [params] - The parameters for this operator.
- * @param {Operator} [source] - The operator from which to receive pulses.
- */
-export default function Transform(init, params) {
-  Operator.call(this, init, null, params);
-}
-
-var prototype = inherits(Transform, Operator);
-
-/**
- * Overrides {@link Operator.evaluate} for transform operators.
- * Marshalls parameter values and then invokes {@link transform}.
- * @param {Pulse} pulse - the current dataflow pulse.
- * @return {Pulse} The output pulse (or StopPropagation). A falsy return
-     value (including undefined) will let the input pulse pass through.
- */
-prototype.evaluate = function(pulse) {
-  var params = this.marshall(pulse.stamp),
-      out = this.transform(params, pulse);
-  params.clear();
-  return out;
-};
-
-/**
- * Process incoming pulses.
- * Subclasses should override this method to implement transforms.
- * @param {Parameters} _ - The operator parameter values.
- * @param {Pulse} pulse - The current dataflow pulse.
- * @return {Pulse} The output pulse (or StopPropagation). A falsy return
- *   value (including undefined) will let the input pulse pass through.
- */
-prototype.transform = function() {};
-```
-
-and as we can see Transform inherits from Operator and so on.
-
-But some of the transform functions looks independent:
-
-Getting cross product:
-
-```javascript
-// filter is an optional  function for selectively including tuples in the cross product.
-function cross(input, a, b, filter) {
-  var data = [],
-      t = {},
-      n = input.length,
-      i = 0,
-      j, left;
-
-  for (; i`, and output them to the `multi-year-report` resource.
-
-The output contains two fields:
-
-- `activity` , which is called `activity` in all sources
-- `amount`, which has varying names in different resources (e.g. `Amount`, `2009_amount`, `amount` etc.)
-
-#### ***`join`***
-
-Joins two streamed resources. 
-
-"Joining" in our case means taking the *target* resource, and adding fields to each of its rows by looking up data in the _source_ resource. 
-
-A special case for the join operation is when there is no target stream, and all unique rows from the source are used to create it. 
-This mode is called _deduplication_ mode - The target resource will be created and  deduplicated rows from the source will be added to it.
-
-_Parameters_:
-
-- `source` - information regarding the _source_ resource
-  - `name` - name of the resource
-  - `key` - One of
-    - List of field names which should be used as the lookup key
-    - String, which would be interpreted as a Python format string used to form the key (e.g. `{}:{field_name_2}`)
-  - `delete` - delete from data-package after joining (`False` by default)
-- `target` - Target resource to hold the joined data. Should define at least the following properties:
-  - `name` - as in `source`
-  - `key` - as in `source`, or `null` for creating the target resource and performing _deduplication_.
-- `fields` - mapping of fields from the source resource to the target resource. 
-  Keys should be field names in the target resource.
-  Values can define two attributes:
-  - `name` - field name in the source (by default is the same as the target field name)
-
-  - `aggregate` - aggregation strategy (how to handle multiple _source_ rows with the same key). Can take the following options: 
-    - `sum` - summarise aggregated values. 
-      For numeric values it's the arithmetic sum, for strings the concatenation of strings and for other types will error.
-
-    - `avg` - calculate the average of aggregated values.
-
-      For numeric values it's the arithmetic average and for other types will err.
-
-    - `max` - calculate the maximum of aggregated values.
-
-      For numeric values it's the arithmetic maximum, for strings the dictionary maximum and for other types will error.
-
-    - `min` - calculate the minimum of aggregated values.
-
-      For numeric values it's the arithmetic minimum, for strings the dictionary minimum and for other types will error.
-
-    - `first` - take the first value encountered
-
-    - `last` - take the last value encountered
-
-    - `count` - count the number of occurrences of a specific key
-      For this method, specifying `name` is not required. In case it is specified, `count` will count the number of non-null values for that source field.
-
-    - `set` - collect all distinct values of the aggregated field, unordered 
-    
-    - `array` - collect all values of the aggregated field, in order of appearance   
-
-    - `any` - pick any value.
-
-    By default, `aggregate` takes the `any` value.
-
-  If neither `name` or `aggregate` need to be specified, the mapping can map to the empty object `{}` or to `null`.
-- `full`  - Boolean,
-  - If `True` (the default), failed lookups in the source will result in "null" values at the source.
-  - if `False`, failed lookups in the source will result in dropping the row from the target.
-
-_Important: the "source" resource **must** appear before the "target" resource in the data-package._
-
-*Examples*:
-
-```yaml
-- run: join
-  parameters: 
-    source:
-      name: world_population
-      key: ["country_code"]
-      delete: yes
-    target:
-      name: country_gdp_2015
-      key: ["CC"]
-    fields:
-      population:
-        name: "census_2015"        
-    full: true
-```
-
-The above example aims to create a package containing the GDP and Population of each country in the world.
-
-We have one resource (`world_population`) with data that looks like:
-
-| country_code | country_name   | census_2000 | census_2015 |
-| ------------ | -------------- | ----------- | ----------- |
-| UK           | United Kingdom | 58857004    | 64715810    |
-| ...          |                |             |             |
-
-And another resource (`country_gdp_2015`) with data that looks like:
-
-| CC   | GDP (£m) | Net Debt (£m) |
-| ---- | -------- | ------------- |
-| UK   | 1832318  | 1606600       |
-| ...  |          |               |
-
-The `join` command will match rows in both datasets based on the `country_code` / `CC` fields, and then copying the value in the `census_2015` field into a new `population` field.
-
-The resulting data package will have the `world_population` resource removed and the `country_gdp_2015` resource looking like:
-
-| CC   | GDP (£m) | Net Debt (£m) | population |
-| ---- | -------- | ------------- | ---------- |
-| UK   | 1832318  | 1606600       | 64715810   |
-| ...  |          |               |            |
-
-
-
-A more complex example:
-
-```yaml
-- run: join
-  parameters: 
-    source:
-      name: screen_actor_salaries
-      key: "{production} ({year})"
-    target:
-      name: mgm_movies
-      key: "{title}"
-    fields:
-      num_actors:
-        aggregate: 'count'
-      average_salary:
-        name: salary
-        aggregate: 'avg'
-      total_salaries:
-        name: salary
-        aggregate: 'sum'
-    full: false
-```
-
-This example aims to analyse salaries for screen actors in the MGM studios.
-
-Once more, we have one resource (`screen_actor_salaries`) with data that looks like:
-
-| year | production                  | actor             | salary   |
-| ---- | --------------------------- | ----------------- | -------- |
-| 2016 | Vertigo 2                   | Mr. T             | 15000000 |
-| 2016 | Vertigo 2                   | Robert Downey Jr. | 7000000  |
-| 2015 | The Fall - Resurrection     | Jeniffer Lawrence | 18000000 |
-| 2015 | Alf - The Return to Melmack | The Rock          | 12000000 |
-| ...  |                             |                   |          |
-
-And another resource (`mgm_movies`) with data that looks like:
-
-| title                     | director      | producer     |
-| ------------------------- | ------------- | ------------ |
-| Vertigo 2 (2016)          | Lindsay Lohan | Lee Ka Shing |
-| iRobot - The Movie (2018) | Mr. T         | Mr. T        |
-| ...                       |               |              |
-
-The `join` command will match rows in both datasets based on the movie name and production year. Notice how we overcome incompatible fields by using different key patterns.
-
-The resulting dataset could look like:
-
-| title            | director      | producer     | num_actors | average_salary | total_salaries |
-| ---------------- | ------------- | ------------ | ---------- | -------------- | -------------- |
-| Vertigo 2 (2016) | Lindsay Lohan | Lee Ka Shing | 2          | 11000000       | 22000000       |
-| ...              |               |              |            |                |                |
-
-
----
-
-### Vega Dataflow usage for DP views
-
-Vega has quite a lot of data transform functions available, however, most of them require complicated JSON descriptor to use. Although we may implement them in the future, at the moment we could start with the most basic and essential ones:
-
-**List of transforms that we could use:**
-
-* Aggregate
-* Filter
-* Formula (applies given formula to dataset)
-* Sample
-
-#### Aggregate example
-
-We have dataset with 4 fields - a, b, c and d. Lets apply different aggregation methods on them - count, sum, min and max:
-
-```javascript
-const vegadataflow = require('./build/vega-dataflow.js');
-
-var tx = vegadataflow.transforms,
-    changeset = vegadataflow.changeset;
-
-var data = [
- {
-   "a": 17.76,
-   "b": 20.14,
-   "c": 17.05,
-   "d": 17.79
- },
- {
-   "a": 19.19,
-   "b": 21.29,
-   "c": 19.19,
-   "d": 19.92
- },
- {
-   "a": 20.33,
-   "b": 22.9,
-   "c": 19.52,
-   "d": 21.12
- },
- {
-   "a": 20.15,
-   "b": 20.72,
-   "c": 19.04,
-   "d": 19.31
- },
- {
-   "a": 17.93,
-   "b": 18.09,
-   "c": 16.99,
-   "d": 17.01
- }
-];
-
-var a = vegadataflow.field('a'),
-    b = vegadataflow.field('b'),
-    c = vegadataflow.field('c'),
-    d = vegadataflow.field('d');
-
-var df = new vegadataflow.Dataflow(),
-    col = df.add(tx.Collect),
-    agg = df.add(tx.Aggregate, {
-            fields: [a, b, c, d],
-            ops: ['count', 'sum', 'min', 'max'],
-            pulse: col
-          }),
-    out = df.add(tx.Collect, {pulse: agg});
-
-df.pulse(col, changeset().insert(data)).run();
-
-console.dir(out.value);
-```
-
-Output:
-```javascript
-[ 
-  {
-    _id: 7, 
-    count_a: 5, 
-    sum_b: 103.14, 
-    min_c: 16.99, 
-    max_d: 21.12 
-  }
-]
-```
-
-#### Filter example
-
-Using the dataset from example above, lets filter values of field `a` that are not greater than 19:
-
-```javascript
-const vegadataflow = require('./build/vega-dataflow.js');
-
-var tx = vegadataflow.transforms,
-    changeset = vegadataflow.changeset;
-
-var data = [
- {
-   "a": 17.76,
-   "b": 20.14,
-   "c": 17.05,
-   "d": 17.79
- },
- {
-   "a": 19.19,
-   "b": 21.29,
-   "c": 19.19,
-   "d": 19.92
- },
- {
-   "a": 20.33,
-   "b": 22.9,
-   "c": 19.52,
-   "d": 21.12
- },
- {
-   "a": 20.15,
-   "b": 20.72,
-   "c": 19.04,
-   "d": 19.31
- },
- {
-   "a": 17.93,
-   "b": 18.09,
-   "c": 16.99,
-   "d": 17.01
- }
-];
-
-var a = vegadataflow.field('a');
-
-var filter1 = vegadataflow.accessor(d => { return d.a > 19 }, ['a']);
-
-var df = new vegadataflow.Dataflow(),
-    ex = df.add(null),
-    col = df.add(tx.Collect),
-    fil = df.add(tx.Filter, {expr: ex, pulse: col}),
-    out = df.add(tx.Collect, {pulse: fil});
-
-df.pulse(col, changeset().insert(data));
-df.update(ex, filter1).run();
-
-console.log(out.value);
-
-```
-
-Output:
-```javascript
-[ 
-  { a: 19.19, b: 21.29, c: 19.19, d: 19.92, _id: 3 },
-  { a: 20.33, b: 22.9, c: 19.52, d: 21.12, _id: 4 },
-  { a: 20.15, b: 20.72, c: 19.04, d: 19.31, _id: 5 } 
-]
-```
-
-#### Formula example
-
-Using the same dataset, lets apply mapping on a field:
-
-```javascript
-const vegadataflow = require('./build/vega-dataflow.js');
-
-var tx = vegadataflow.transforms,
-    changeset = vegadataflow.changeset;
-
-var data = [
- {
-   "a": 17.76,
-   "b": 20.14,
-   "c": 17.05,
-   "d": 17.79
- },
- {
-   "a": 19.19,
-   "b": 21.29,
-   "c": 19.19,
-   "d": 19.92
- },
- {
-   "a": 20.33,
-   "b": 22.9,
-   "c": 19.52,
-   "d": 21.12
- },
- {
-   "a": 20.15,
-   "b": 20.72,
-   "c": 19.04,
-   "d": 19.31
- },
- {
-   "a": 17.93,
-   "b": 18.09,
-   "c": 16.99,
-   "d": 17.01
- }
-];
-
-
-var df = new vegadataflow.Dataflow(),
-    e = vegadataflow.field('e'),
-    f = vegadataflow.field('f'),
-    formula1 = vegadataflow.accessor(d => { return d.a * 10; }, ['a']),
-    formula2 = vegadataflow.accessor(d => { return d.b / 10; }, ['b']),
-    col = df.add(tx.Collect),
-    fa = df.add(tx.Formula, {expr: formula1, as: 'e', pulse: col}),
-    fb = df.add(tx.Formula, {expr: formula2, as: 'f', pulse: fa});
-
-df.pulse(col, changeset().insert(data)).run();
-
-console.log(col.value.map(e));
-console.log(col.value.map(f));
-```
-
-Output:
-```
-[ 177.60000000000002, 191.9, 203.29999999999998, 201.5, 179.3 ]
-[ 2.0140000000000002, 2.129, 2.29, 2.072, 1.809 ]
-```
-
-#### Sample example
-
-Lets create a dataset with 100 rows and take a sample of 10 from it:
-
-```javascript
-const vegadataflow = require('./build/vega-dataflow.js');
-
-var tx = vegadataflow.transforms,
-    changeset = vegadataflow.changeset;
-
-var n = 100,
-    sampleSize = 10,
-    data = Array(n),
-    i;
-
-for(i=0; i 10'
-  },
-  ...
-}
-```
-For `filter` type expression should evaluate to true or false so only truthy values will be kept.
-
-#### Formula
-
-```javascript
-{
-  ...
-  transform: {
-    type: 'formula',
-    expr: ['data.fieldName * 2', 'data.fieldName + 10'],
-    as: ['x', 'y']
-  },
-  ...
-}
-```
-
-For `formula` type, a field will be mapped with given expression and output will be stored in new fields that are specified in `as` property.
-
-#### Sample
-
-```javascript
-  ...
-  transform: {
-    type: 'sample',
-    size: 'some integer'
-  },
-  ...
-```
-
-In `sample` type, only size of a sample is needed.
-
diff --git a/site/content/docs/dms/datahub/developers/views.md b/site/content/docs/dms/datahub/developers/views.md
deleted file mode 100644
index 65a81f88..00000000
--- a/site/content/docs/dms/datahub/developers/views.md
+++ /dev/null
@@ -1,168 +0,0 @@
-# Views
-
-Producers and consumers of data want to have data presented in tables and graphs -- "views" on the data. They want this for a range of reasons, from simple eyeballing to drawing out key insights.
-
-```mermaid
-graph LR
-  data[Your Data] --> table[Table]
-  data --> grap[Graph]
-  data --> geo[Map]
-```
-
-To achieve this we need to provide:
-
-* A tool-chain to create these views from the data.
-* A descriptive language for specifying views such as tables, graphs, map.
-
-These requirements are addressed through the introduction of Data Package "Views" and associated tooling.
-
-```mermaid
-graph LR
-
-  subgraph Data Package
-    resource[Resource]
-    view[View]
-    resource -.-> view
-  end
-
-  view --> toolchain
-  toolchain --> svg["Rendered Graph (SVG)"]
-  toolchain --> table[Table]
-  toolchain --> map[Map]
-```
-
-This section describes the details of how we support [Data Package Views][views] in the DataHub.
-
-It consists of two parts, the first describes the general tool chain we have. The second part describes how we use that to generate graphs in the showcase page.
-
-**Quick Links**
-
-* [Data Package Views introduction and spec][views]
-* [datapackage-render-js][] - this is the library that implements conversion from the data package views spec to vega/plotly and then svg or png
-
-[views]: /docs/dms/publishers/views
-[datapackage-render-js]: https://github.com/frictionlessdata/datapackage-render-js
-[dpr-js]: https://github.com/frictionlessdata/dpr-js
-
-## The Tool Chain
-
-***Figure 1: From Data Package View Spec to Rendered output***
-
-```mermaid
-graph TD
-  pre[Pre-cursor views e.g. Recline] --bespoke conversions--> dpv[Data Package Views]
-  dpv --"normalize (correct any variations and ensure key fields are present)"--> dpvn["Data Package Views
(Normalized)"] - dpvn --"compile in resource & data ([future] do transforms)"--> dpvnd["Self-Contained View
(All data and schema inline)"] - dpvnd --compile to native spec--> plotly[Plotly Spec] - dpvnd --compile to native spec--> vega[Vega Spec] - plotly --render--> html[svg/png/etc] - vega --render--> html -``` - -**IMPORTANT**: an important "convention" we adopt for the "compiling-in" of data is that resource data should be inlined into an `_values` attribute. If the data is tabular this attribute should be an array of *arrays* (not objects). - -### Graphs - -***Figure 2: Conversion paths*** - -```mermaid -graph LR - inplotly["Plotly DP Spec"] --> plotly[Plotly JSON] - simple[Simple Spec] --> plotly - simple .-> vega[Vega JSON] - invega[Vega DP Spec] --> vega - vegalite[Vega Lite DP Spec] --> vega - recline[Recline] .-> simple - plotly --plotly lib--> svg[SVG / PNG] - vega --vega lib--> svg - - classDef implemented fill:lightblue,stroke:#333,stroke-width:4px; - class recline,simple,plotly,svg,inplotly,invega,vega implemented; -``` - -Notes: - -* Implemented paths are shown in lightblue - code for this is in [datapackage-render-js][] -* Left-most column (Recline): pre-specs that we can convert to our standard specs -* Second-from-left column: DP View spec types. -* Second-from-right column: the graphing libraries we can use (which all output to SVG) - -### Geo support - -**Note**: support for customizing map is limited to JS atm - there is no real map "spec" in JSON yet beyond the trivial version. - -**Note**: vega has some geo support but geo here means full geojson style mapping. - -```mermaid -graph LR - - geo[Geo Resource] --> map - map[Map Spec] --> leaflet[Leaflet] - - classDef implemented fill:lightblue,stroke:#333,stroke-width:4px; - class geo,map,leaflet implemented; -``` - -### Table support - -```mermaid -graph LR - resource[Tabular Resource] --> table - table[Table Spec] --> handsontable[HandsOnTable] - table --> html[Simple HTML Table] - - classDef implemented fill:lightblue,stroke:#333,stroke-width:4px; - class resource,table,handsontable implemented; -``` - -### Summary - -***Figure 3: From Data Package View to Rendered output flow (richer version of diagram 1)*** - - - - -## Views in the Showcase - -To render Data Packages in browsers we use DataHub views written in JavaScript. The module implemented in ReactJS framework and it can render tables, maps and various graphs using third-party libraries. - -Implementing code can be found in: - -* [dpr-js repo][dpr-js] - which in turn depends on [datapackage-render-js][] - -```mermaid - graph TD - - url["metadata URL passed from back-end"] - dp-js[datapackage-js] - dprender[datapackage-render-js] - table["table view"] - chart["graph view"] - hot[HandsOnTable] - map[LeafletMap] - vega[Vega] - plotly[Plotly] - browser[Browser] - - url --> dp-js - dp-js --fetched dp--> dprender - dprender --spec--> table - table --1..n--> hot - dprender --geojson--> map - dprender --spec--> chart - chart --0..n--> vega - chart --0..n--> plotly - hot --table--> browser - map --map--> browser - vega --graph--> browser - plotly --graph--> browser -``` - -Notice that DataHub views render a table view per tabular resource. If GeoJSON resource is given, it renders a map. Graph views should be specified in `views` property of a Data Package. - -## Appendix - -There is a separate page with [additional research material regarding views specification and tooling][views-research]. - -[views-research]: /docs/dms/datahub/developers/views-research - diff --git a/site/content/docs/dms/datahub/v3.md b/site/content/docs/dms/datahub/v3.md deleted file mode 100644 index 0c3c7ac0..00000000 --- a/site/content/docs/dms/datahub/v3.md +++ /dev/null @@ -1,184 +0,0 @@ -# DataHub v3 (Next) - -## Introduction - -Overview of the third generation DataHub. In planning since 2019 this will launch in 2021. For background on v1 and v2 and how we came to v3 see [History section below](#history). - -## What - -Make it stupidly easy, fast and reliable to share your data in a **useable*** way**. - -* It is already easy to "share" data: just use dropbox, google drive, s3, github etc. However, it’s not so easy to share it in a way that’s usable e.g. with descriptions for the columns, data that’s viewable and searchable (not just raw), with clearly associated docs, with an API etc. - -** Not only with others but with *yourself*. This may sound a bit odd: don’t you already have the data? What we mean is, for example, going from a raw CSV to a browseable table (share it with your "eyes") or converting it to an API so that you can use it in a data driven app or analysis (sharing from one tool to another). - -Make it easy **for whom**? Power users like data engineers and data scientists. People familiar with a command line and github. - -### An Analogy - -There is a useful analogy with Vercel (Zeit). Vercel focuses on webapp deployment and developer experience. We focus on data "deployment" and data (wrangler) experience. - -| Vercel | DataHub | -|--------|---------| -| A platform for deploying webapps (esp Next.JS) with a focus on simplicity, speed and DX | A platform for "deploying" datasets with a focus on simplicity, speed and DX (data experience). Deploying = a "portal-like" presentation of the data plus e.g. APIs, workflows etc. | - -Aside: in a further analogy, we will also have an open-source data presentation framework "Portal.JS" which has some analogies with Next.JS: - -| Vercel | DataHub | -|--------|---------| -| **Next.JS**: a framework for building webapps (with react) | **Portal.JS**: the data presentation framework (a framework for presenting data(sets) and building data-driven micro web apps | - - -## Features - -`data` will be used throughout for the DataHub command line tool. - -* Get a Portal (Present your data) - * Local (Preview) - * Deployed online -* Data API - * (?) Local - * Deployed online -* Hub: management UI (and API) - -### Portal - -I have a dataset `my-data` - -```bash -README.md -data.csv -## descriptor is optional (we infer if not there) -# datapackage.json -``` - -I can do: - -```bash -cd my-dataset -data portal -``` - -And I get a nice dataset page like this available locally at e.g. http://localhost:3000: - -![](https://i.imgur.com/KSEtNF1.png) - -#### Details - -* Elegant presentation -* Shows the data in a table etc (searchable / filterable) - * Supports other data formats e.g. json, xlsx etc -* Show graphs (vega, plotly) -* Show maps -* Data summary -* Works with … - * README + csv - * Frictionless dataset - * Frictionless resource - * pure README with frontmatter - -Bonus - -* Copes with lots of data files -* (?) Git history if you have it … (with data oriented support e.g. diffs) -* ?? (for local not sure?) gives me a queryable api with the data … (< 100MB) - -Bonus ++: - -* Customizable themes - -### Deploy - -```bash -cd my-data -data deploy -``` - -Gives me a url like: - -``` -https://dataset.owner.datahub.io -``` - -#### Details - -* Deploys a shareable url with the content of show - * Semi-private - * Can integrate access control (?) -* Deploys a data API -* [Other integrations e.g. push to google spreadsheets] - -### API - -Run `data deploy` and you get an API to your data that you can others can use (as well as the portal): - -```bash -https://dataset.owner.datahub.io/api -# query it ... -https://dataset.owner.datahub.io/api?file=data.csv&q=abc -``` - -NB: this is tabular data only (or JSON data in tabular structure.) - -#### Details - -* GraphQL by default (?) - * Maybe a basic -* Also can expose raw SQL -* Get an API explorer -* API shows up in portal presentation -* Can customize the API with a yml file - -Bonus - -* Authentication -* Usage tracking -* Billing per usage - -### Hub - -I can login at datahub.io and go to datahub.io/dashboard and I have a Dashboard showing all my DataHub projects - -### Github integration - -Push to github and automatically deploy to DataHub. - -#### Details - -* Overview and add/track your GitHub projects from DataHub dashboard. -* In the future, we may add other platforms like GitLab etc. -* Deploy on every push to any branch - * Main branch => production => main URL - * Other branches => with hash or branch name => branchOrHash.dataset.username.datahub.io -* Create a new project/dataset from a template (?) -* Have a status check in the GitHub UI that shows if your deployment succeeded/failed/pending - - -## History - -### v1 DataHub(.io) - -This was the original CKAN.net starting from 2007 up until circa 2016. It was powered by CKAN and changed from CKAN.net to DataHub.io (first thedatahub.org) around 2012. - -### v2 DataHub(.io) - -In 2016-2017 DataHub v2 was created and launched. This was a rewrite from scratch with a vision of a next generation DataHub. Datasets were all Frictionless, all data was stored (no more metadata only datasets), there was built in data workflows for processing data including validation, data summarization, CSV to JSON. Visualizations were supported, completely new and elegant UI, command line by default (in fact no UI for creating datasets though one was planned). - -v2 was actively worked on from ~2016 to late 2018. It was a major advance technically and product wise on the old DataHub. However, it did not get traction and was rather complex (even if simpler than old CKAN): not only did we build presentation but we were also building our own data factory from the ground up plus doing versioning. There are other people doing parts of that better (we even argued for using airflow vs build our own factory at the time). In addition, i would observe that: - -* Code goes with the data so you want to keep them together -* People want to use their existing tools (e.g. pandas or airflow vs another ETL system) -* People want to keep their data (and code) in their “system” if possible (for security, compliance, privacy etc) - -We were trying to solve several different problems (with very limited resource): - -* Data showcasing -* Lightweight ETL (data factory) -* Data deployment (some combination of the above) -* Marketplace - -In particular, we never resolved an ambiguity slightly ambiguous whether we were a data marketplace (we tried this as a pivot for some period from late 2017) or a data publishing platform. - -### v3 DataHub - -We've been thinking about a v3 of DataHub since 2019. Originally, the core idea was to retain catalog aspect but move to being more git(hub) backed. See for example this (deprecated) outline idea for v3: https://github.com/datopian/datahub-next (NB: Git(hub) backed data portals remain an active idea and as of Q1 2021 we've implemented one and plan more. However it is not the focus for DataHub but is instead probably part of the Portal.JS and CKAN evolution). \ No newline at end of file diff --git a/site/content/docs/dms/dms.md b/site/content/docs/dms/dms.md deleted file mode 100644 index 5797daf9..00000000 --- a/site/content/docs/dms/dms.md +++ /dev/null @@ -1,87 +0,0 @@ -# DMS (Data Management System) - -This document is an introduction to the technical design of Data Management Systems (DMS). This also covers Data Portals since Data Portals are one major solution one can build with a data management system. - -## Domain Model - -* Project: a data project. It has has a single dataset in the same way GitHub or Gitlab "project" has a single repo. Traditionally in, say CKAN, this has been implicit and identified with the dataset. There are, however, important differences: a project can include a dataset but also other related functionality such as issues, workflows etc. -* Dataset: a set of data, usually zero or more resources. -* Resource (or File): a single data object. - -Revisioning - -* Revision -* Tag -* (Branch) - -Presentation - -* View -* Showcase -* Data API - -Identity and Permissions - -* Account -* Profile -* Permission - -Data Factory - -* Task -* DAG (Pipeline) -* Run (Job) - -### GraphQL version - -```graphql= -type Project { - id: ID! - description: String - readme: String - dataset: Dataset - views: [View] - issues: [Issue] - actions: [Action] -} - -type Dataset { - # data package descriptor structure - id: ID - name: String - ... - resources: [Resource] -} - -type Resource { - # follows Frictionless Resource - path: ... - id: ... - name: ... - schema: Schema -} - -# Table Schema usually ... -type Schema { - -} - -# dataset view e.g. table, graph, map etc -type View { - id: ID! -} -``` - -## Actions / Flows [component] - -* View Dataset: [Showcase page] a page displaying the dataset (or a resource) - * View a Revision / Tag / Branch: -* Add / Upload: ... -* Tag - -## Components - -* **Meta~~Store~~Service**: stores dataset metadata (and revisions) -* **HubStore**: stores all the users, organizations and their connections to the datasets. -* **SearchStore + Service**: search index and API -* **BlobStore**: stores blobs (for files) diff --git a/site/content/docs/dms/flows.md b/site/content/docs/dms/flows.md deleted file mode 100644 index ca999754..00000000 --- a/site/content/docs/dms/flows.md +++ /dev/null @@ -1,94 +0,0 @@ -# Data Processing: Data Flows and Data Factories - -## Introduction - -A common aspect of data management is **processing** data in some way or another: cleaning it, converting it from one format to another, integrating different datasets together etc. Such processing usually takes place in what are termed data (work)flows or pipelines. Each flow or pipeline consists of one or more stages with one particular operation (task) being done with the data at each stage. Finally, there is a need for something to manage and orchestrate the data flows/pipelines. This overall system which includes both the flows themselves and the framework for managing them needs a name. We call it a "Data Factory". - -Let's have some concrete examples of simple pipelines: - -* Loading a raw CSV file into a database (e.g. to power the data API) -* Converting a file from one format to another e.g. CSV to JSON -* Loading a file, validating it and then computing some summary statistics - -Fig 1: A simple data pipeline to clean up a CSV file - -```mermaid -graph TD - -source[Source data e.g. load from CSV] -t1[Transform 1 e.g. delete trailing rows] -t2[Transform 2 e.g. lower case everything] -sink[Write output to CSV] - -source -- resource --> t1 --resource--> t2 --resource--> sink -``` - -### Domain Model - -* Tasks: a single processing step that is applied to data -* Flows (DAGs): a flow or pipeline of tasks. These tasks form a "directed acyclic graph" (DAG) where each task is a node. -* Factory: a system for creating and managing (orchestrating, monitoring etc) those flows. - -Each flow or pipeline consists of one or more stages with one particular operation (task) being done with the data at each stage. - -In a basic setup a flow is linear: the data arrives, operation A happens, then operation B, and, finally operation C. However, more complex flows/pipelines can involve branching e.g. the data arrives, then operation A, then there is a branch and operation B and C can happen independently. - -**Fig 2: An illustration of a Generic Branching Flow (DAG)** - -```mermaid -graph TD - -a[Source] --> b -a --> c -b --> d -c --> d -d --> sink[Sink] -``` - -## CKAN v2 - -CKAN v2 has two implicit data factory system embedded in other functionality. These systems are technically entirely independent: - -* Data Load system for loading data to the DataStore -- see [Data Load page »](/docs/dms/load/) -* Harvesting system for importing dataset metadata from other catalogs -- see [Harvesting page »](/docs/dms/harvesting/) - -## CKAN v3 - -The Data Factory system is called AirCan and is built on top of AirFlow. AirCan itself can be used on its own or integrated with CKAN. - -AirCan: - -* Runner: Apache AirFlow -* Pipelines/DAGs: https://github.com/datopian/aircan. This is a set of AirFlow DAGs designed for common data processing operations in a DMS such as loading data into Data API storage. - -CKAN integration: - -* CKAN extension: https://github.com/datopian/ckanext-aircan. This hooks key actions from CKAN into AirCan and provides an API to run the flows (DAGs). -* GUI: Under development. - -**Status**: Beta. AirCan and ckanext-aircan are in active use in production. GUI is under development. - -**Documentation**: including setup and use of the all the components including CKAN integration can be found in https://github.com/datopian/aircan - - -### Design - -See [Design page](/docs/dms/flows/design). - - -## Links - -* [Research](/docs/dms/flows/research) - list of tools and concepts with some analysis -* [History](/docs/dms/flows/history) - some previous thinking and work (2016-2019) - - -## Appendix: Our Previous Work - -See also [History page](/docs/dms/flows/history). - -* http://www.dataflows.org/ - new system Adam + Rufus designed in spring 2018 and Adam led development on - * https://github.com/datahq/dataflows - * https://github.com/datahq/dataflows/blob/master/TUTORIAL.md - * https://github.com/datahq/dataflows/blob/master/PROCESSORS.md -* https://github.com/datopian/dataflow-demo - Rufus outline on a very simple tool from April 2018 -* https://github.com/datopian/factory - datahub.io Factory "The service is responsible for running the flows for datasets that are frequently updated and maintained by Datahub. Service is using Datapackage Pipelines is a framework for declarative stream-processing of tabular data, and DataFlows to run the flows through pipelines to process the datasets." diff --git a/site/content/docs/dms/flows/airtunnel.png b/site/content/docs/dms/flows/airtunnel.png deleted file mode 100644 index 187218c89b267563feeb0be6a2f9d53bde730c4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221659 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�DM{O#LX|8a{6*E-4zB{+-rc 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 ZddT+M)v>f5Oc4Y^TH>X6v6#Wz{|DXsjwk>C diff --git a/site/content/docs/dms/flows/design.md b/site/content/docs/dms/flows/design.md deleted file mode 100644 index aed0bc4d..00000000 --- a/site/content/docs/dms/flows/design.md +++ /dev/null @@ -1,225 +0,0 @@ -# Data Factory Design - -Our Data Factory system is called AirCan. A Data Factory is a set of services/components to process and integrate data (coming from different sources). Plus patterns / methods for integrating with CKAN and the DataHub. - -## Components - -```mermaid -graph LR - -subgraph Orchestration - airflow[AirFlow] - airflowservice[AirFlow service] -end - -subgraph CKAN integration - ckanhooks[CKAN extension to trigger and report on factory activity] - ckanapi[API for triggering DAGs etc] - ckanui[UI integration - display info on ] -end - -subgraph Processors and Flows - ckandatastoreload[CKAN Loader lib] - ckanharveters[CKAN Harvesters] - validation[Validation Lib] -end -``` - -## DataStore Load job story - -### Reporting Integration - -When I upload a file to CKAN and it is getting loaded to the datastore (automatically), I want to know if that succeeded or failed so that I can share with my users that the new data is available (or do something about the error). - -For a remote Airflow instance (let's say on Google Composer), describe the DAG tasks and the process. i.e. - -* File upload on CKAN triggers the ckanext-aircan connector -* which makes API request to airflow on GCP and triggers a DAG with following parameters - * A f11s resource orject including - * the remote location of the CSV file and the reource ID - * The target resource id - * An API key to use when loading to CKAN datastore - * [A callback url] -* The DAG - * deletes the datatore table - * if it exists, creates a new datastore table - * loads CSV from the specified location (inforation available on DAG parameters) - * converts the CSV to JSON. The output of the converted JSON file will be in a bucket on GCP. - * upserts the JSON data row by row into the CKAN DataStore via CKAN's DataStore API - * This is what we have now: {'invoke{"message":"Created "}'} `/api/3/action/datastore_create` and passing the contents of the json file - * OR using upsert with inserts (faster) NB: datapusher just pushes the whole thing into `datastore_create` so stick with that. - * OR: if we are doing postgres copy we need direct access to postgres DB - * ... [tbd] notifies CKAN instance of this (?) - -Error Handling and other topics to consider - -* How can we let CKAN know something went wrong? Shall we create a way to notify a certain endpoint on ckannext-aircan connector? -* Shall we also implement a timeout on CKAN? -* What are we going to display in case of an error? -* The "tmp" bucket on GCP will eventually get full of files; shall we flush it? How do we know when it's safe to delete a file? - * Lots of ways up this mountain. -* What do we do for large files? - -## AirCan API - -AirCan is built on AirFlow so we have same basic API TODO: insert link - -However, we have standard message formats to pass to DAGs following these principles: All dataset and data resource objects should following the Frictionless specs - -Pseudo-code showing how we call the API: - -```python= -airflow.dag_run({ - "conf": { - "resource": json.dumps({ # f11s resource object - resource_id: ... - path: ... - schema: ... - }) - "ckan_api_key: ... - "ckan_api_endpoint": demo.ckan.org/api/ - } -}) -``` - -See for latest, up to date version: https://github.com/datopian/ckanext-aircan/blob/master/ckanext/aircan_connector/action.py#L68 - -## CKAN integration API - -There is a new API as follows: - -`http://ckan:5000/api/3/action/aircan_submit?dag_id=...&dataset=...&resource` - -Also DAGs can get triggered on events ... TODO: go look at Github actions and learn from it ... - -## Architecture - -Other principles of architecture: - -* AirFlow tasks and DAGs should do very little themselves and should hand off to separate libraries. Why? To have better separation of concerns and **testability**. AirCan is reasonably cumbersome to test but an SDK is much more testable. - * Thus AirFlow tasks are often just going to pass through arguments TODO: expand this with an example ... -* AirFlow DAG will have incoming data and config set in "global" config for the DAG and so available to every task ... -* Tasks should be as decoupled as possible. Obviously there *is* some data and metadata passing between tasks and that should be done by writing those to a storage bucket. Metadata MUST be stored in f11s format. - * See this interesting blog post (not scientific) about why the previous approach, with side effcts, is not very resilient in the long run of a project https://medium.com/@maximebeauchemin/functional-data-engineering-a-modern-paradigm-for-batch-data-processing-2327ec32c42a - * don't pass data explicitly between tasks (rather it is passed implicitly via an expectation of where the data is stored ...) - * tasks and flows should be re-runnable ... (no side effects principle) - -Each task can write to this location: - -``` -bucket/dagid/runid/taskid/resource.json -bucket/dagid/runid/taskid/dataset.json -bucket/dagid/runid/taskid/... # data files -``` - - -## UI in DMS - -URL structure on a daaset - -``` -# xxx is a dataset -/@myorg/xxx/actions/ -/@myorg/xxx/actions/runs/{id} -``` - -Main question: to display to user we need some way to log what jobs are associated with what datasets (and users) and perhaps their status - -* we want to keep factory relatively dumb (it does not know about datasets etc etc) -* in terms of capabilities we need a way to pass permissions into the data factory (you hand over the keys to your car) - -Simplest approach: - -* MetaStore (CKAN metadata db) has Jobs table which have structure of `| id | factory_id | job_type | created | updated | dataset | resource | user | status | info |` (where info is json blob) - * status = one of `WAITING | RUNNING | DONE | FAILED | CANCELLED`. If failed we should have stuff in info about that. - * `job_type` = one of `HARVEST | LOAD | VALIDATE ...` it is there so we could have several different factory jobs in one db - * `info`: likely stuff - * run time - * error information (on failure) - * success information: what was outcome, where are outputs if any etc -* On creating a job in the factory, the factory returns a factory id. the metastore stores the factory id into a new job object along with dataset and user info ... - * Qu: why have id and factory_id separate? is there any situation where you have a job w/o a factory id? -* Then on loading a job page in frontend you can poll the factory for info and status (if status is WAITING or RUNNING) - * => do we need the `info` column on the job (it's just a cache of this info)? - * Ans: useful for jobs which are complete so we don't keep polling the factory (esp if factory deletes stuff) -* Can list all jobs for a given dataset (or resource) with info about them - -Qus: - -* For Data Factory what do I do with Runs that are stale etc - how do I know who they are associated with. Can I store metadata on my Runs like who requested it etc. - -### UI Design - -Example from Github: - -![](https://i.imgur.com/xnTRq5T.png) - -## Appendix - -### Notes re AirCan API - -https://medium.com/@ptariche/interact-with-apache-airflows-experimental-api-3eba195f2947 - -``` -{"message":"Created "} - -GET /api/experimental/dags//dag_runs/ - -GET /api/experimental/dags/ckan_api_load_gcp/dag_runs/2020-07-14 13:04:43+00:00 - -https://b011229e45c662be6p-tp.appspot.com/api/experimental/dags/ckan_api_load_gcp/dag_runs/2020-07-14T13:04:43+00:00 - -Resp: `{"state":"failed"}` -``` - -### Google Cloud Composer - -Google Cloud Composer is a hosted version of AirFlow on Google Cloud. - -#### How Google Cloud Composer differs from local AirFlow - -* File handling: On GCP, all the file handling must become interaction with a bucket ~rufus: what about from a url online (but not a bucket) -Specifying the csv resource location (on a local Airflow) must become sending a resource to a bucket (or just parsing it from the JSON body). When converting it to a JSON file, it must become an action of creating a file on a bucket. -* Authentication: TODO - -### AirFlow Best Practices - -* Should you and how do you pass information between tasks? - * https://medium.com/datareply/airflow-lesser-known-tips-tricks-and-best-practises-cf4d4a90f8f - * https://towardsdatascience.com/airflow-sharing-data-between-tasks-7bbaa27eeb1 - -### What terminology should we use? - -ANS: we use AirFlow terminology: - -* Task -* DAG -* DagRun - -For internals what are the options? - -* Task or Processor or ... -* DAG or Flow or Pipeline? - -TODO: table summarizing options in AirFlow, Luigi, Apache Beam etc. - -#### UI Terminology - -* Actions -* Workflows - -Terminology options - -* Gitlab - * Pipelines: you have - * Jobs (runs of those - * Schedules -* Github - * Workflows - * Runs - * (Schedules - not explicit) -* Airflow - * DAGs - * Tasks - * DAG Runs - diff --git a/site/content/docs/dms/flows/history.md b/site/content/docs/dms/flows/history.md deleted file mode 100644 index 9da8c5f1..00000000 --- a/site/content/docs/dms/flows/history.md +++ /dev/null @@ -1,517 +0,0 @@ -# Data Factory and Flows Design - Oct 2017 to Apr 2018 - -Date: 2018-04-08 - -> [!note] ->This is a miscellaneous content from various HackMD docs. I'm preserving because either a) there is material to reuse here that I'm not sure is elsewhere b) there were various ideas in here we used later (and it's useful to see their origins). -> ->Key content: -> ->* March-April 2018: first planning of what became dataflows (had various names including dataos). A lot of my initial ideas ended up in this micro-demo https://github.com/datopian/dataflow-demo. This evolved with Adam into https://github.com/datahq/dataflows ->* Autumn 2017: planning of Data Factory which was the data processing system inside DataHub.io. This was more extensive than dataflows (e.g. it included a runner, an assembler etc) and was based original data-package-pipelines and its runner. Issues with that system was part of the motivation for starting work on dataflows. -> ->~Rufus May 2020 - - - -## Plan April 2018 - -* tutorial (what we want our first post to look like) - * And then implement minimum for that -* programmatic use of pipelines and processors in DPP - * processor abstraction defined ... - * DataResource and DataPackage object that looks like [Frictionless Lib pattern][frictionless-lib] - * processors library split out - * code runner you can call dataos.run.runSyncPipeline -* dataflow init => python and yaml -* @adam Write up Data Factory architecture and naming as it currently stands [2h] - -[frictionless-lib]: http://okfnlabs.org/blog/2018/02/15/design-pattern-for-a-core-data-library.html - -## 8 April 2018 - -Lots of note on DataFlow which are now moved and refactored into https://github.com/datahq/dataflow-demo - -The Domain Model of Factory - -* Staging area -* Planner -* Runner -* Flow - * Pipelines - * Processors - -```mermaid -graph TD - -flow[Flow] -pipeline[Pipelines] -processor[Processors] - -flow --> pipeline -pipeline --> processor -``` - -Assembler ... - -```mermaid -graph LR - -source[Source from User
source.yaml] --assembler--> plan[Execution Plan
DAG of pipelines] -plan --> pipeline[Pipeline Runner
Optimizer/Dependency management] -``` - -=> Assembler generates a DAG. - -- dest filenames in advance ... -- for each pipeline: pipelines it dpeends on - - e.g. sqlite: depends on on all derived csv pipelines running - - node depends: all csv, all json pipelines running - - zip: depends on all csv running -- Pipelines - - -```mermaid -graph LR - -source[Source Spec Parse
validate?] --> planner[Planner] -planner --> workplanb(Plan of Work

DAG of pipelines) - -subgraph Planner - planner - subplan1[Sub Planner 1 e.g. SQLite] -end -``` - - -## 5 Oct 2017 - -Notes: - -* Adam: finding more and more bugs (edge cases) and then applying fixes but then more issues - * => Internal data model of pipelines was wrong ... - * Original data model has a store: with one element the pipeline + a state (its idle, invalid, waiting to be executed, running, dirty) - * Problem starts: you have a very long pipeline ... - * something changes and pipeline gets re-added to the queue. then you have same pipeline in queue in two different states. Should not be a state of the pipeline but state of the execution of the pipeline. - * Split model: pipeline (with their hash) + "runs" ordered by time of request - -Questions: - -* Tests in assembler ... - -### Domain Model - -```mermaid -graph TD - -flow[Flow] -pipeline[Pipelines] -processor[Processors] - -flow --> pipeline -pipeline --> processor -``` - -* Pipelines have no branches they are always linear - * Input: nothing or a file or a datapackage (source is stream or nothing) - * Output: datapackage - usually dumped to something (could be stream) - * Pipelines are a list of processors **and** their inputs -* A Flow is a DAG of pipelines - * In our case: one flow produces a "Dataset" at a given "commit/run" -* Source Spec + DataPackage[.json] => (via assembler) => Flow Spec -* Runner - * Pipelines runner: a set of DAG of pipelines (where each pipeline is schedule to run once all dependencies have been run) -* Events => lead to new flows or pipelines being created ... (or existing ones being stopped or destroyed) - -```mermaid -graph LR - -subgraph flow2 -x --> z -y --> z -end - -subgraph flow1 -a --> c -b --> c -end -``` - -State of Factory(flows, state) - -f(event, state) => state - -flow dependencies? - -Desired properties - -* We economise on runs: we don't rerun processors (pipelines?) that have same config and input data - * => one reason for breaking down into smaller "operators" is that we economise here ... -* Simplicity: the system is understandable ... -* Processors (pipelines) are atomic - they get their configuration and run ... -* We can generate from a source spec and an original datapackage.json a full set of pipelines / processors. -* Pipelines as Templates vs Pipelines as instances ... - -pipeline id = hash(pipeline spec, datapackage.json) - -{'{pipelineid}'}/... - -next pipeline can rely on {'{pipelineid}'} - -Planner ... - -=> a pipeline is never rerun (once it is run) - -| | Factory | Airflow | -|--------------|----------------------------|-----------| -| DAG | Implicit (no concept) | DAG | -| Node | Pipelines (or processors?) | Operators | -| Running Node | ? Running pipelines | Tasks | -| Comments | | | - -https://airflow.incubator.apache.org/concepts.html#workflows - -# Analysis from work for Preview - -As a Publisher I want to upload a 50Mb CSV so that the showcase page works - it does not crash by browser (because it is trying to load and display 50Mb of CSV) - -*plus* - -As a Publisher I want to customize whether I generate a preview for a file or not so that I don't get inappropriate previews - -> As a Publisher I want to have an SQLite version of my data auto-built -> -> As a Publisher I want to upload an Excel file and have csv versions of each sheet and an sqlite version - -### Overview - -*This is what we want* - -```mermaid -graph LR - -subgraph Source - file[vix-daily.csv] - view[timeseries-1
Uses vix-daily] -end - -subgraph "Generated Resources" - gcsv[derived/vix-daily.csv] - gjson[derived/vix-daily.json] - gjsonpre[derived/vix-daily-10k.json] - gjsonpre2[derived/view-time-series-1.json] -end - -subgraph "Generated Views" - preview[Table Preview] -end - -file --rule--> gcsv -file --rule--> gjson -file --rule--> preview - -view --> gjsonpre2 -preview --> gjsonpre -``` - -### How does this work? - -#### Simple example: no previews, single CSV in source - -```mermaid -graph LR - -load[Load Data Package
Parse datapackage.json] -parse[Parse source data] -dump((S3)) - -load --> parse -parse --> dcsv -parse --> djson -parse --> dumpdp -dcsv --> dump -djson --> dump -dsqlite --> dump -dnode --> dump -dumpdp --> dump - -dcsv --> dnode -dcsv --> dsqlite - -subgraph "Dumpers 1" - dcsv[Derived CSV] - djson[Derived JSON] -end - -subgraph "Dumpers 2 - after Dumper 1" - dsqlite[SQLite] - dnode[Node] -end - - dumpdp[Derived DataPackage.json

Assembler gives it the DAG info
Runs after everything
as needs size,md5 etc] -``` - -```yaml= -meta: - owner: - ownerid: - dataset: - version: 1 - findability: -inputs: - - # only one input is supported atm - kind: datapackage - url: - parameters: - resource-mapping: - : - -outputs: - - ... see https://github.com/datahq/pm/issues/17 -``` - -#### With previews - -```mermaid -graph LR - -load[Load Data Package
Parse datapackage.json] -parse[Parse source data] -dump((S3)) - -load --> parse -parse --> dcsv -parse --> djson -parse --> dumpdp -dcsv --> dump -djson --> dump -dsqlite --> dump -dnode --> dump -dumpdp --> dump - -dcsv --> dnode -dcsv --> dsqlite - -parse --> viewgen -viewgen --> previewgen -previewgen --view-10k.json--> dump - -subgraph "Dumpers 1" - dcsv[Derived CSV] - djson[Derived JSON] -end - -subgraph "Dumpers 2 - after Dumper 1" - dsqlite[SQLite] - dnode[Node] - viewgen[Preview View Gen
Adds preview views] - previewgen[View Resource Generator] -end - - dumpdp[Derived DataPackage.json

Assembler gives it the DAG info
Runs after everything
as needs size,md5 etc] -``` - -### With Excel (multiple sheets) - -Source is vix-daily.xls (with 2 sheets) - - -```mermaid -graph LR - -load[Load Data Package
Parse datapackage.json] -parse[Parse source data] -dump((S3)) - -dumpdp[Derived DataPackage.json

Assembler gives it the DAG info
Runs after everything
as needs size,md5 etc] - -load --> parse - -parse --> d1csv -parse --> d1json -parse --> d2csv -parse --> d2json - -parse --> dumpdp - -d1csv --> dump -d1json --> dump -d2csv --> dump -d2json --> dump - -dsqlite --> dump -dnode --> dump -dumpdp --> dump - -d1csv --> dnode -d1csv --> dsqlite -d2csv --> dnode -d2csv --> dsqlite - -d1csv --> view1gen -d2csv --> view2gen -view1gen --> preview1gen -view2gen --> preview2gen -preview1gen --view1-10k.json--> dump -preview2gen --view2-10k.json--> dump - -subgraph "Dumpers 1 sheet 1" - d1csv[Derived CSV] - d1json[Derived JSON] -end - -subgraph "Dumpers 1 sheet 2" - d2csv[Derived CSV] - d2json[Derived JSON] -end - -subgraph "Dumpers 2 - after Dumper 1" - dsqlite[SQLite] - dnode[Node] - view1gen[Preview View Gen
Adds preview views] - preview1gen[View Resource Generator] - view2gen[Preview View Gen
Adds preview views] - preview2gen[View Resource Generator] -end -``` - -datapackage.json - -```javascript= -{ - resources: [ - { - "name": "mydata" - "path": "mydata.xls" - } - ] -} -``` - -```yaml= -meta: - owner: - ownerid: - dataset: - version: 1 - findability: -inputs: - - # only one input is supported atm - kind: datapackage - url: - parameters: - resource-mapping: - : - -processing: - - - input: # mydata - output: # mydata_sheet1 - tabulator: - sheet: 1 - - - input: # mydata - output: # mydata_sheet2 - tabulator: - sheet: 2 -``` - -```yaml= -meta: - owner: - ownerid: - dataset: - version: 1 - findability: -inputs: - - # only one input is supported atm - kind: datapackage - url: - parameters: - resource-mapping: - : // excel file - -=> (implictly and in cli becomes ...) - -... - -processing: - - - input: # mydata - output: # mydata-sheet1 - tabulator: - sheet: 1 -``` - -Result - -```javascript= -{ - resources: [ - { - "name": "mydata", - "path": "mydata.xls" - }, - { - "path": "derived/mydata.xls.sheet1.csv" - "datahub": { - "derivedFrom": "mydata" - } - }, - { - "path": "derived/mydata.xls.sheet2.csv" - "datahub": { - "derivedFrom": "mydata" - } - } - ] -} -``` - -### Overall component design - -Assembler ... - -```mermaid -graph LR - -source[Source from User
source.yaml] --assembler--> plan[Execution Plan
DAG of pipelines] -plan --> pipeline[Pipeline Runner
Optimizer/Dependency management] -``` - -=> Assembler generates a DAG. - -- dest filenames in advance ... -- for each pipeline: pipelines it dpeends on - - e.g. sqlite: depends on on all derived csv pipelines running - - node depends: all csv, all json pipelines running - - zip: depends on all csv running -- Pipelines - - -```mermaid -graph LR - -source[Source Spec Parse
validate?] --> planner[Planner] -planner --> workplanb(Plan of Work

DAG of pipelines) - -subgraph Planner - planner - subplan1[Sub Planner 1 e.g. SQLite] -end -``` - -### NTS - -``` -function(sourceSpec) => (pipelines, DAG) - -pipeline - - pipeline-id - steps = n * - processor - parameters - schedule - dependencies -``` diff --git a/site/content/docs/dms/flows/research.md b/site/content/docs/dms/flows/research.md deleted file mode 100644 index 9330fd3b..00000000 --- a/site/content/docs/dms/flows/research.md +++ /dev/null @@ -1,109 +0,0 @@ -# Data Flows + Factory - Research - -## Tooling - -* Luigi & Airflow - * These are task runners - managing a dependency graph between 1000s of tasks. - * Neither of them focus on actual data processing and are not a data streaming solution. Tasks do not move data from one to the other. - * AirFlow: see further analysis below -* Nifi: Server based, Java, XML - not really suitable for quick prototyping -* Cascading: Only Java support -* Bubbles http://bubbles.databrewery.org/documentation.html - https://www.slideshare.net/Stiivi/data-brewery-2-data-objects -* mETL https://github.com/ceumicrodata/mETL mito ETL (yaml config files) -* Apache Beam: see below -* https://delta.io/ - acid for data lakes (mid 2020). Comes out of DataBricks. Is this pattern or tooling? - - -## Concepts - -* Stream and Batch dichotomy is probably a false one -- and unhelpful. Batch is just some grouping of stream. Batch done regularly enough starts to be a stream. -* More useful is complete vs incomplete data sources -* Hard part of streaming (or batch) work is handling case where events arrive "late". For example, let's say i want to total up total transaction volume at a bank per day ... but some transactions arrived at the server late e.g. a transaction at 2355 actually arrives at 1207 because of network delay or some other issue then if i batch at 1200 based on what has arrived i have an issue. Most of work and complexity in Beam / DataFlow model relates to this. -* Essential duality between flows and states via difference and wum. E.g. transaction and balance: - * Balance over time -- differenced --> Flow - * Flow -- summed --> Balance -* Balance is often just a cached "sum". -* Also relevant to datsets: we often think of them as states but really they are a flow. - -### Inbox - -* [x] DataFlow paper: "The Dataflow Model: A Practical Approach to BalancingCorrectness, Latency, and Cost in Massive-Scale,Unbounded, Out-of-Order Data Processing" (2015) -* [ ] Stream vs Batch - * [x] Streaming 101: The world beyond batch. A high-level tour of modern data-processing concepts. (Aug 2015) https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101 **Good intro to streaming and DataFlow by one of its authors** - * [ ] https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102 Follow up to previous paper -* [ ] Apache Beam **in progress -- see below* -* [ ] dbt **initial review. Mainly a way conventient way of tracking in DB transforms** -* [ ] Frictionless DataFlows -* [x] Kreps (kafka author): https://www.oreilly.com/radar/questioning-the-lambda-architecture/ - * lambda architecture is where you run both batch and streaming in parallel as way to have traditional processing plus some kind of real-time results. - * basically Kreps says its a PITA to keep two parallel systems running and you can just go "streaming" (remember we are beyond the dichotomy) - -## Apache Beam - -https://beam.apache.org/blog/2017/02/13/stateful-processing.html - -### Pipeline - -https://beam.apache.org/releases/pydoc/2.2.0/apache_beam.pipeline.html - -Pipeline, the top-level Beam object. - -A pipeline holds a DAG of data transforms. Conceptually the nodes of the DAG are transforms (PTransform objects) and the edges are values (mostly PCollection objects). The transforms take as inputs one or more PValues and output one or more PValue s. - -The pipeline offers functionality to traverse the graph. The actual operation to be executed for each node visited is specified through a runner object. - -Typical usage: - -```python -# Create a pipeline object using a local runner for execution. -with beam.Pipeline('DirectRunner') as p: - - # Add to the pipeline a "Create" transform. When executed this - # transform will produce a PCollection object with the specified values. - pcoll = p | 'Create' >> beam.Create([1, 2, 3]) - - # Another transform could be applied to pcoll, e.g., writing to a text file. - # For other transforms, refer to transforms/ directory. - pcoll | 'Write' >> beam.io.WriteToText('./output') - - # run() will execute the DAG stored in the pipeline. The execution of the - # nodes visited is done using the specified local runner. -``` - -## Airflow - -Airflow organices tasks in a DAG. A DAG (Directed Acyclic Graph) is a collection of all the tasks you want to run, organized in a way that reflects their relationships and dependencies. - -* Each task could be Bash, Python or others. -* You can connect the tasks in a DAG as you want (which one depends on which). -* Tasks could be built from Jinja templates. -* It has a nice and comfortable UI. - -You can also use _Sensors_: you can wait for certain files or database changes for activate anoter jobs. - -References - -* https://github.com/apache/airflow -* https://medium.com/videoamp/what-we-learned-migrating-off-cron-to-airflow-b391841a0da4 -* https://medium.com/@rbahaguejr/airflow-a-beautiful-cron-alternative-or-replacement-for-data-pipelines-b6fb6d0cddef - - -### airtunnel - -https://github.com/joerg-schneider/airtunnel - -* https://medium.com/bcggamma/airtunnel-a-blueprint-for-workflow-orchestration-using-airflow-173054b458c3 - excellent piece on how to pattern airflow - "airtunnel", plus overview of key tooling - - > This is why we postulate to have a central declaration file (as in YAML or JSON) per data asset, capturing all these properties required to run a generalized task (carried out by a custom operator). In other words, operators are designed in a generic way and receive the name of a data asset, from which they can grab its declaration file and learn how to parameterize and carry out the specific task. - -``` -├── archive -├── ingest -│ ├── archive -│ └── landing -├── ready -└── staging - ├── intermediate - ├── pickedup - └── ready -``` diff --git a/site/content/docs/dms/frictionless.md b/site/content/docs/dms/frictionless.md deleted file mode 100644 index 5958c06f..00000000 --- a/site/content/docs/dms/frictionless.md +++ /dev/null @@ -1,56 +0,0 @@ -# Frictionless Data and Data Packages - -## What's a Data Package? - -A [Data Package](https://frictionlessdata.io/data-package/) is a simple container format used to describe and package a collection of data (a dataset). - -A Data Package can contain any kind of data. At the same time, Data Packages can be specialized and enriched for specific types of data so there are, for example, Tabular Data Packages for tabular data, Geo Data Packages for geo data etc. - -## Data Package Specs Suite - -When you look more closely you'll see that Data Package is actually a *suite* of specifications. This suite is made of small specs, many of them usuable on their own, that you can also combine together. - -This approach also reflects our philosophy of "small pieces, loosely joined" as well as "make the simple things simple and complex things possible": it easy to just use the piece you need as well to scale up to more complex needs. - -For example, the basic Data Package spec can be combined with Table Schema spec for tabular data (plus CSV as the base data format) to create the Tabular Data Package specification. - -We also decomposed the overall Data Package spec into Data Package and Data Resource with the Data Resource spec just describing an individual file and a Data Package being a collection of one or more Data Resources with additional dataset-level metadata. - -**Example: Data Resource spec + Table Schema spec becomes a Tabular Data Resource spec** - -```mermaid -graph TD - - dr[Data Resource] --add table schema--> tdr[Tabular Data Resource] -``` - -**Example: How a Tabular Data Package is composed out of other specs** - -```mermaid -graph TD - - dr[Data Resource] --> tdr - tdr[Tabular Data Resource] --> tdp[Tabular Data Package] - dp[Data Package] --> tdp - jts[Table Schema] --> tdr - csvddf[CSV Data Descriptor] --> tdr - - style tdp fill:#f9f,stroke:#333,stroke-width:4px; -``` - -Two different logics of grouping: - -* By function e.g. Tabular stuff ... - * Tabular Data package - * Tabular Data resource -* Inheritance / Composition structure - * Resource -> Tabular Data Resource - * Data Package -> Tabular Data Package - -For developers of the specs latter may be better. - -For ordinary users I imagine the former is better. - -## Tutorials - -Data Package Find-Prepare-Share Guide: https://datahub.io/docs/getting-started/datapackage-find-prepare-share-guide diff --git a/site/content/docs/dms/frontend/index.md b/site/content/docs/dms/frontend/index.md deleted file mode 100644 index 35153dd0..00000000 --- a/site/content/docs/dms/frontend/index.md +++ /dev/null @@ -1,113 +0,0 @@ -# Frontend - -The (read) frontend component covers all of the traditional "read" frontend functionality of a data portal: front page, searching datasets, viewing datasets etc. - ->[!tip]Announcing Portal.js📣 ->Portal.js 🌀 is a javascript framework for building rich data portal frontends fast using a modern frontend approach (JavaScript, React, SSR). -> ->https://github.com/datopian/portal.js - - -## Features - -* **Home Page** When visiting the data portal's home page, I want to see an overview of the portal (e.g. datasets) so that I understand if it's relevant for me. -* **Search/Browse the Catalog** When looking for dataset, I want to search for specific strings (keywords, topics etc.) so that I can find it quickly, if available. -* **Dataset Showcase** When exploring a dataset I want to see a description and key information (title etc) and (if possible) data preview and download options so that I understand what it contains and decide if I want to use it or download it. -* **Organization and User Profiles**: I want to see the data published by a particular team, organization or user so that I can find the data I want or understand what a particular group are doing -* **Groups/Topics/Collections**: I want to browse datasets by topic so that I can find the data I want or find unexpected results -* **Custom additional pages** -* **Permissions**: I want to restrict access to some of the above based on a user's role and memberships so that I can share data with only the appropriate people - -Developer Experience - -* **Theme** (simple): When developing a new portal, I want to theme it quickly and easily so that it's look and feel aligned to the client's needs. - * **Customize Home Page**: When building a data portal home page I want to be able to customize it completely, integrating different widgets so that I have a great landing experience for users - * **i18n**: I want to be able to i18n content and enable the client to do this so that we have a site in the client's locale -* **Rich customization** (new routes, major page changes) - * When working on a data portal, I want to add frontend functionality to existing templates so that I can build upon past work and still extend the project to my own needs. - * When building up a new frontend I want to quickly add standard pages and components (and tweak them) so that I have a basic functional site quickly -* **Use common languages and tooling**: When working on a data portal, I want to build it using Javascript so that I can rely on the latest frontend technologies (frameworks/libraries). -* **Deploy quickly**: When delivering a data portal, I want to quickly and easily deploy changes to my frontend so that I can reduce the feedback loop. - -## CKAN v2 - -The Frontend is implemented in the core app spread across various controllers, templates etc. For extending/theming a template, you have to write an extension (`ckanext-mysite`), and either override or inherit from the default files. - -* Home page. The CKAN default template shows: Site title, Search element, The latest organizations, The latest groups. In order to change this, we need to create a CKAN extension and modify templates etc. -* Search/Browse the Catalog Already available in CKAN Classic (v2) with ability to search by facets etc., see an example here - https://demo.ckan.org/dataset -* Dataset Showcase. It is already available by default, for example: - * Dataset page - https://demo.ckan.org/dataset/dataset_389383 - a summary of resources and package level metadata such as package title, description, license etc. - * Package controller - https://github.com/ckan/ckan/blob/master/ckan/controllers/package.py - * Package view module - https://github.com/ckan/ckan/blob/master/ckan/views/dataset.py - * Resource page - https://demo.ckan.org/dataset/dataset_389383/resource/331f57d1-74fc-46ad-9885-50eb26dde13a - showcase of individual resource including views etc. - * Resource view module - https://github.com/ckan/ckan/blob/master/ckan/views/resource.py - * Package and resource templates - https://github.com/ckan/ckan/tree/master/ckan/templates/package - -### Developer Experience (DX) - -Docs - https://docs.ckan.org/en/2.8/theming/index.html - -* You need to do it in a new CKAN extension and follow recommended standards. There are no easy ways of reusing code from other projects, since most often they are not written in the required languages/frameworks/libraries. -* Nowdays, the best to do it is to create an extension for each of the components. -* There's no easy documented path for achieving this. -* The easier way is to deploy a complete CKAN v2 stack using Docker Compose. - -- Theming - https://docs.ckan.org/en/2.8/theming/index.html -- Create new helper functions https://docs.ckan.org/en/2.8/theming/templates.html#template-helper-functions - -### Theming - -Theming is done via CKAN Classic extensions. See https://docs.ckan.org/en/2.8/theming/index.html - -### Extending (Plugins) - -In CKAN Classic you extend the frontend e.g. adding new pages or altering existing ones by a) overriding templates b) creating an extension using specific plugin points (e.g. IController): https://docs.ckan.org/en/2.8/extensions/index.html - -### Limitations - -There are two main issues: - -* There is no standard, satisfactory way to create data portals that integrates data and content. Current methods for integrating CMS content into CKAN are clunky and limited. -* Theming and frontend work is slow, painful and difficult for standard frontend ddvs because a) it requires installing and interacting with the full (complex) CKAN b) you use very specific frontend stack (python etc rather than javascript) c) template spaghetti (the curse of a million "slots") (did inheritance rather tha composition) -* There is too much coupling of frontend and backend e.g. logic layer doing dictize. Poor separation of concerns. - -In more detail: - -* Theming - styling, templating: - * It uses Bootstrap 3 (out-dated). An upgrade takes significant amount of effort because all the existing themes rely, or may rely, on it. - * No documented way of switching Bootstrap off and replace it for another framework. - * Although the documentation only mentions pure CSS, CKAN also uses LESS. It's not clear how a theme could be written in LESS, if recommended or possible. - * For changing or adding a better overview, one needs to create a CKAN extension, with its own challenges. - * It needs to happen in Python/Jinja, overriding the exting actions and templates. - * The main challenge is general theming in CKAN Classic, e.g., you have to follow the CKAN Classic way using inheritance model of templates. -* Javascript: - * No viable way of extending it in other languages such as Javascript. - * It's not simple to achieve the common task of adding Javascript to the frontend. - * You must understand CKAN and a large portion of its architecture. - * You must run CKAN in its entirety. - * The document is far from short – https://github.com/ckan/ckan/blob/2.8/doc/theming/javascript.rst - * Not (easily, at least) possible to develop a Single Page Application while still relying on CKAN for all the backend. - -* Other: - * It's not easy to make configuration changes to how the existing feature works. - * The dataset URL follows a nested RESTFul format, with non-human readable IDs. - * Not good for SEO. - * It may be a reasonable default, but hardly works in practice as stakeholders have their own preferences. - -## CKAN v3 - ->[!tip]Announcing Portal.js📣 ->Portal.js 🌀 is a javascript framework for building rich data portal frontends fast using a modern frontend approach (JavaScript, React, SSR). -> ->https://github.com/datopian/portal.js - -Previous (stable) version is `frontend-v2`: https://github.com/datopian/frontend-v2. It is written in NodeJS using ExpressJS. For templating it uses [Nunjucks][]. - -[Nunjucks]: https://mozilla.github.io/nunjucks/templating.html - ->[!note]It is easy to write your own Next Gen frontend in any language or framework you like -- much like the frontend of a headless CMS site. And obviously you can still reuse the patterns (and even code if you are using JS) from the default approach presented here. - - -## RFC - -Background and motivation in the RFC https://github.com/ckan/ideas/blob/master/rfcs/0005-decoupled-frontend.md diff --git a/site/content/docs/dms/giftless.md b/site/content/docs/dms/giftless.md deleted file mode 100644 index 7baf4c10..00000000 --- a/site/content/docs/dms/giftless.md +++ /dev/null @@ -1,190 +0,0 @@ -# Giftless - the GIT LFS server - -## Introduction - -Our work on Giftless started from the context of two distinct needs: - -* Need: direct to (cloud) storage uploading (and download) including from client => you need a service that will issue tokens to upload direct to storage -- what we term a "Storage Access Gateway" =>The Git LFS server protocol actually provides this with its `batch` API. Rather than reinventing the wheel let's use this existing protocol. -* Need: git is already widespread and heavily used by data scientists and data engineers. However, git does not support large files well whilst data work often involves large files. Git LFS is the protocol designed to support large file storage stored outside of git blobstore. If we have our own git lfs server then we can integrate any storage we want with git. - -From these we arrived at a vision for a standalone Git LFS server (standalone in contrast to the existing git lfs servers provided as an integrated part of existing git hosting providers such as github or gitlab). We also wanted to be able to customize it so it could be backed onto any major cloud storage backend (e.g. S3, GCS, Azure etc). We also had a preference for Python. - -**Why build something new?** We looked around at the existing [Git LFS server implementations][impl] and couldn't find one that looked like it suit our needs: there were only a few standalone servers, only one in Python, and those that did exist were usually quite out of date and supported old versions of the LFS protocol (see appendix below for further details). - -[impl]: https://github.com/git-lfs/git-lfs/wiki/Implementations - -## Giftless API - -Giftless follows the [gif-lfs API][lfsapi] in general, with the following differences and extensions: - -* Locking: no support at present -* Multipart: giftless adds support for multi-part transfers. See XXX for details -* Giftless adds optional support for `x-filename` object property, which allows specifying the filename of an object in storage (this allows storage backends to set the "Content-disposition" header when the file is downloaded via a browser, for example) - -Below we summarize the key API endpoints. - -[lfsapi]: https://github.com/git-lfs/git-lfs/tree/master/docs/api - -### `POST /foo/bar/objects/batch` - -``` -{ - "transfers": ["multipart-basic", "basic"], - "operation": "upload", - "objects": [ - { - "oid": "20492a4d0d84f8beb1767f6616229f85d44c2827b64bdbfb260ee12fa1109e0e", - "size": 10000000 - } - ] -} -``` - -### Optional API endpoints - -The following endpoints are also exposed by Giftless, but may not be used in some workflows, depending on your setup: - -#### `POST /foo/bar/objects/storage/verify` - -Verify an object in storage; This is used by different storage backends to check -a file after it has been uploaded, corresponding to the Git LFS `verify` action. - -#### `PUT /foo/bar/objects/storage` - -Store an object in local storage; This is only used if Giftless is configured to also -act as the storage backend server, which is not a typical production setup. This accepts -the file to be uploaded as HTTP request body. - -An optional `?jwt=...` query parameter can be added to specify a JWT auth token, if JWT -auth is in use. - -#### `GET /foo/bar/objects/storage` - -Fetch an object from local storage; This is only used if Giftless is configured to also -act as the storage backend server, which is not a typical production setup. This will -return the file contents. - -An optional `?jwt=...` query parameter can be added to specify a JWT auth token, if JWT -auth is in use. - -### Comment: why the slightly weird API layout - -The essence of giftless is to hand out tokens to store or get data in blob storage. One would anticipae the basic API to be a bit simpler e.g. something like implemented in our earlier effort `bitstore`: https://github.com/datopian/bitstore#get-authorized-upload-urls - -```json= -POST /authorize - -{ - "metadata": { - "owner": "", - "name": "" - }, - "filedata": { - "filepath": { - "length": 1234, #length in bytes of data - "md5": "", - "type": "", - "name": "" - }, - "filepath-2": { - "length": 4321, - "md5": "", - "type": "", - "name": "" - } - ... - } -} -``` - -However, the origins of git lfs with github means that it has a slightly odd setup whereby the start of the url is `/foo/bar` corresponding to `{org}/{repo}`. - -## Mapping from Giftless to Storage - -One important question when using giftless is how a file maps from api call to storage ... - -This call: - -``` -/foo/bar/objects/batch - -oid: (sha256) xxxx -``` - -Maps in storage to ... - -``` -storage-bucket/{prefix}/foo/bar/xxx -``` - -Where `{prefix}` is configured as env variable to Giftless server (and can be empty) - - -## Authentication and Authorization - -How does giftless determine that you are allowed to upload to - -``` -/foo/bar/objects/batch - -oid: XXXX -``` - -Does that based on scopes on jwt token ... - -See https://github.com/datopian/giftless/blob/feature/21-improve-documentation/docs/source/auth-providers.md for more details. - - -## Use with git - -https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md - -Set `.lfsconfig` - -## Appendix: Summary of Git LFS - -https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md - -* TODO: sequence diagram of git interaction with gif lfs server -* TODO: summary of server API - -### Summary of interaction (for git client) - -* Perform server discovery to discover the git lfs server: https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md - * Use `.lfsconfig` if it exists - * Default: Git LFS appends .git/info/lfs to the end of a Git remote url to build the LFS server URL it will use: -* Authentication (?) -* Send a `batch` API call to the server configured in the Git client's .lfsconfig (TODO: verify config location) - * The specifics of the `batch` request depend on the current operation being performed and the objects (that is files) operated on; -* The response to `batch` includes "instructions" on how to download / upload files to storage - * Storage can be on the same server as Git LFS (in some dedicated endpoints) or on a whole different server, for example S3 or Google Cloud Storage - * Git LFS defines a few "transfer modes" which define how to communicate with the storage server; The most basic mode (known as `"basic"`), uses HTTP GET and PUT to download / upload the files given a URL and some headers. - * There could be other transfer modes - for example, Giftless defines a custom transfer mode called `multipart-basic` which is specifically designed to take advantage of Cloud storage vendors' multipart upload features. -* Based on the transfer mode & instructions (typically URL & headers) specified in the response to the `batch` call, the git lfs client will now interact with the storage server to upload / download files - -### How git lfs works for git locally - -* Have special git lfs blob storage in git directory -* On checkout pull from that blob store rather than standard one -* on Add and commit write a pointer file inito "git" tree instead of actual file and put file in git lfs blob storage -* On push: push git lfs blobs to blob server -* On pull: pull git lfs blobs that i need for current checkout - * TODO: do i pull other stuff? - -### Git LFS Server API - -https://github.com/git-lfs/git-lfs/tree/master/docs/api - -### Batch API - -https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md - -TODO: summarize here. - -## Appendix: Existing Git LFS server implementations - -A review of some of the existing GIT LFS server implementations. - -* https://github.com/kzwang/node-git-lfs Node, well developed but now archived and last updated 5y ago. This implementation provided inspiration for giftless. -* https://github.com/mgax/lfs: Python, only speaks legacy v1 API and last updated properly ~2y ago -* https://github.com/meltingice/git-lfs-s3 - Ruby, repo is archived. Last updated 6y ago. diff --git a/site/content/docs/dms/glossary.md b/site/content/docs/dms/glossary.md deleted file mode 100644 index 45e602ab..00000000 --- a/site/content/docs/dms/glossary.md +++ /dev/null @@ -1,39 +0,0 @@ -# Glossary - -[Resource]: #Resource - -## Data Management System - -A Data Management System is a *framework* for building data management solutions such as data catalogs, data portals, data factories, data workflows and various combinations and extensions of these. - -## Dataset - -Dataset is a collection of related and (potentially) interconnected [Resource][]s. Example: Excel file with mulitple sheets, Database etc. - -## DMS - -DMS is an acronym for Data Management System. - -## File - -Usually a *data* file. See Resource. - -## Profile - -The structure for general metadata for data. E.g. this dataset follows the "Biodiversity Data Publication v1.3 Profile". - -## Resource - -Resource (aka File) is a single data file or object. Strictly, the Resource should correspond to a single logical data structure e.g. a single table vs multiple tables. Example: CSV file, single sheet spreadsheet, geojson file. - -Confusingly, an actual physical file/resource can correspond to multiple logical resources e.g. an Excel file with multiple sheets corresponds conceptually to a (logical) Dataset with multiple (logical) Resources. - -## Schema - -A schema for data (specifically a resource). For example: - -* The set of fields present e.g the columns in the spreadsheet -* Thee type of each field e.g. is this column a string, number, date etc -* Other restrictions e.g. all values in this field are positive - -See Frictionless Table Schema for a detailed spec: http://frictionlessdata.io/table-schema/ diff --git a/site/content/docs/dms/harvesting.md b/site/content/docs/dms/harvesting.md deleted file mode 100644 index bd424e5e..00000000 --- a/site/content/docs/dms/harvesting.md +++ /dev/null @@ -1,658 +0,0 @@ -# Harvesting - -## Introduction - -Harvesting is the automated collection into a Data Portal of metadata (and maybe data) from other catalogs and sources. - -The core epic is: As a Data Portal Manager I want to harvest datasets' metadata (and maybe data) from other portals into my portal so that all the metadata is in one place and hence searchable/discoverable there - -### Features - -Key features include: - -* Harvest from multiple sources and with a variety of source metadata formats (e.g. data.json, DCAT, CKAN etc). - * Implied is the ability to create and maintain (generic) harvesters for different types of metadata (e.g. data.json, DCAT) (below we call these pipelines) - * Off-the-shelf harvesting for common metadata formats e.g. data.json, DCAT etc -* Incremental, efficient harvesting from a given source. For example, imagine a source catalog that has ~100k datasets and adds 100 new datasets every day. Assuming you have already harvested this catalog, you only want to harvest those 100 new datasets during your daily harvest (and not re-harvest all 100k). Similarly, you want to be able to handle deletions and modifications of existing datasets. - * And even more complex case is where the harvested metadata is edited in the harvesting catalog and one has to handle merging of changes from the source catalog into the harvesting catalog (i.e. you can handle changes in both locations). -* Create and update harvest sources via API and UI -* Run and view harvests via API (and UI) and the background logging and monitoring to support that -* Detailed and useful feedback of harvesting errors so that harvest maintainers (or downstream catalog maintainers) can quickly and easily diagnose and fix issues -* Robust and reliable performance, for example supporting harvesting thousands or even millions of datasets a day - -### Harvesting is ETL - -"Harvesting" of metadata in its essence is exactly the same as any data collection and transformation process ("ETL"). "Metadata" is no different from any other data for our purposes (it is just quite small!). - -This insight allows us to see harvesting as just like any other ETL process. At the same time, the specific nature of *this* ETL process e.g. that it is about collecting dataset metadata, allows us to design the system in specific ways. - -We can use standard ETL tools to do harvesting, loosely coupling their operation to the CKAN Classic (or CKAN Next Gen) metastore. - -### Domain Model - -The Harvesting Domain Model has the following conceptual components: - -* **Pipeline**: a generic "harvester" pipeline for harvesting a particular data type e.g. data.json, dcat. A pipeline consists of Processors. -* **Source (aka Harvester)**: the entire spec of a repeatable harvest from a given source including the pipeline to use, the source info and any additional configuration e.g. the schedule on which to run this -* **Run (Job)**: a given run of a Source -* **Dataset**: a resulting dataset. -* **Log (Entry)**: (including errors) - -NB: the term harvester is often used both for a pipeline (e.g. the DCAT Harvester) and for a Source e.g. "XYZ Agency data.json Harvester". Because of this confusion we prefer to avoid the term, or to reserve it for an active Source e.g. "the GSA data.json harvester". - -### Components - -A Harvesting system has the following key subsystems and components: - -#### ETL - -* **Pipelines**: a standard way of creating pipelines and processors in code -* **Runner**: a system for executing Runs of the harvesters. This should be queue based. -* **Logging**: a system for logging (and reporting) including of errors -* **Scratch (Store)**: Intermediate storage for temporary or partial outputs of the -* **API**: interfaces the runner and errors - -#### Sources and Configuration - -* **Source Store**: database of Sources -* **API/UI**: UI, API and CLI usually covering Source Store plus reporting on them e.g. Runs, Errors etc - -#### UI (web, command line etc) - -* User interface (web and/or command line etc) to ETL e.g. runner, errors -* User interface to sources and configuration - -#### MetaStore - -* **MetaStore**: the store for harvested metadata -- this is considered to be outside the harvesting system itself - -{/* */} - - -## CKAN v2 - -CKAN v2 implements harvesting via [ckanext-harvest extension][ckanext]. - -This extension stores configuration in the main DB and harvesters run off a queue process. A detailed analysis of how it works is in [the appendix below](#appendix-ckan-classic-harvesting-in-detail). - -[ckanext]: https://github.com/ckan/ckanext-harvest - -### Limitations - -The main problem is that ckanext-harvest builds its own bespoke mini-ETL system and builds this into CKAN. A bespoke system is less powerful and flexible, harder to maintain etc and building it in makes CKAN more bulky (conceptually, resource wise etc) and creates unnecessary coupling. - -Good: Using CKAN as config store and having a UI for that - -Not so good: - -* An ETL system integrated with CKAN - * Tightly coupled: so running tests of ETL requires CKAN. This makes it harder to creaate tests (with the result that many harvests have few or no tests). - * Installation is painful (CKAN + 6 steps) making it harder to use, maintain and contribute to - * Dependent on CKAN upgrade cycles (tied via code rather than service API) - * CKAN is more complex -* Bespoke ETL system is both less powerful and harder to maintain - * For example, the Runner is bespoke to CKAN rather than using something standard like e.g. Airflow - * Rigid structure (gather, fetch, import) which may not fit many situations e.g. data.json harvesting (one big file with many datasets where everything done in gather) or where dependencies between stages -* Logging and reporting is not great and hard to customize (logs into CKAN) -* Maintenance Status - Some maintenance but not super it looks like but quite a lot outstanding (as of Aug 2019): - * 47 [open issues](https://github.com/ckan/ckanext-harvest/issues) - * 6 [open pull requests](https://github.com/ckan/ckanext-harvest/pulls) (some over a year old) - - -## CKAN v3 - -Next Gen harvesting decouples the core "ETL" part of harvesting into a small, self-contained microservice that is runnable on its own and communicates with the rest of CKAN over APIs. This is consistent with the general [next gen microservice approach](ckan-v3). - -The design allows the Next Gen Harvester to be used with both CKAN Classic and CKAN Next Gen. - -Perhaps most important of all, the core harvester can use standard third-party patterns and tools to make it both more powerful, easier to maintain and easier to use. For example, it can use Airflow for its runner rather than a bespoke system built into CKAN. - -### Features - -Specific aspects of the next gen approach: - -* Simple: Easy to write harvesters -- just a python script and you can create harvesters without needing to know almost anything about CKAN -* Runnable and testable standalone (without the rest of CKAN) which makes running and testing much easier -* Uses the latest standard ETL techniques and technologies -* Multi-cluster support: run one harvester for multiple CKAN instances -* Data Package based - -### Design - -Here is an overview of the design. Further details on specific parts e.g. Pipelines in following sections. Coloring indicates implementation status: - -* Green: Implemented -* Pink: In progress -* Grey: Next up - -```mermaid -graph TD - -ui[WUI to CRUD Sources] -runui[Run UI] -config(Harvest Source API) -metastore(MetaStore API for
harvested metadata) -logging(Reporting API) -runner[Runner + Queue - Airflow] -runapi[Run API - Logic of Running Jobs etc] -pipeline[Pipeline System + Library] - -subgraph "CKAN Classic" - ui --> config - metastore -end - -subgraph "Next Gen Components" - runui --> runapi - runapi --> runner - pipeline -end - -pipeline --> metastore -config -.-> runapi -runner --> pipeline -pipeline -.reporting/errors.-> logging - -subgraph "Pipeline System + Library" - framework[Framework
DataFlows + Data Packages] --> dataerror[Data Errors] - dataerror --> ckansync[CKAN Syncing] - ckansync --> datajson[data.json impl] -end - -classDef done fill:lightgreen,stroke:#333,stroke-width:2px; -classDef existsneedsmod fill:blue,stroke:#333,stroke-width:2px; -classDef started fill:yellow,stroke:#333,stroke-width:2px; -classDef todo fill:orange,stroke:#333,stroke-width:2px; - -class config,metastore,runner,ui,datajson,ckansync,framework,pipeline done; -class runapi started; -class runui,logging todo; -``` - - -### User Journey - -* Harvest Curator goes to WUI for Sources and does Create Harvest Source ... -* Fills it in ... -* Comes back to the harvest source dashboard -* To run a harvest source you go to the new Run UI interface - * It lists all harvest sources like the harvest source ... -* Click on Run (even if this is just to set up the schedule ...) -* Go to Job page for this run which shows the status of this run and any results ... -* [TODO: how do we link up harvest sources to runs] - -### Pipelines - -These follow a standard pattern: - -* Built in Python -* Use DataFlows by default as way to structure the pipeline (but you can use anything) -* Produce data packages at each step -* Pipelines should be divided into two parts: - * Extract and Transform: (fetch and convert) fetching remote datasets and converting them into a standard intermediate form (Data Packages) - * Load: loading that intermediate form into the CKAN instance. This includes not only the format conversion but the work to synchronize state i.e. to create, update or delete in CKAN based on whether a given dataset from the source already has a representation in CKAN (harvests run repeatedly). - -```mermaid -graph LR - -subgraph "Extract and Transform" - extract[Extract] --> transform[Transform] -end - -subgraph "Load" - transform --data package--> load[Load] -end - -load --> ckan((CKAN)) -``` - -This pattern has these benefits: - -* Load functionality to be reused across Harvest Pipelines (the same Load functionality can be used again and again) -* Cleaner testing: you can test extract and load without needing to have a CKAN instance -* Ability to reuse Data Package tooling - -#### Pipeline Example: Fetch and process a data.json - -* **Extract**: Take say a data.json - * Validate -* **Transform**: Split into datasets and then transform into data packages - * Save to local -* **Transform 2** check the difference and write to the existing DB. -* **Load**: Write to DB (CKAN metastore / DB) - -```mermaid -graph TD - -get[Retrieve data.json] -validate[Validate data.json] -split[Split data.json into datasets] -datapkg[Convert each data.json dataset into Data Package] -write[Write results somewhere local] -ckan[Sync to CKAN - work out diff and implement] - -get --> validate -validate --> split -split --> datapkg -datapkg --> write -write --> ckan -``` - -#### Pipeline example detailed - -```mermaid -graph TD - -download[Download data] -fetch[Fetch] -validate[Validate data] -get_previous_datasets(Get previous harvested data) -save_download_results(Save as Data Package) -compare(Compare) -save_compare_results(Save compare results) -write_destination(Write to destination) -save_previous_data(Save as Data Package) -save_log_report(Save final JSON log) -federal[Federal] -non_federal[Non Federal] -dataset_adapter[Dataset Adapter] -resource_adapter[Resource Adapter] - -classDef green fill:lightgreen; -class download,fetch,validate,get_previous_datasets,save_download_results,compare,save_compare_results,write_destination,save_previous_data,save_log_report,federal,non_federal,dataset_adapter,resource_adapter green; - -subgraph "Harvest source derivated (data.json, WAF, CSW)" - download - save_download_results -end - -subgraph "Harvester core" - fetch - subgraph Validators - validate - federal - non_federal - end - subgraph Adapters - dataset_adapter - resource_adapter - end -end - -subgraph "Harvest Source base class" - save_download_results - compare - save_compare_results - save_log_report -end - -subgraph "Harvest Destination" - get_previous_datasets - write_destination - save_previous_data -end - -download -.transform to general format.-> save_download_results -save_download_results --> compare -compare --> save_compare_results -save_compare_results --> dataset_adapter -dataset_adapter --> resource_adapter -resource_adapter --> write_destination - -get_previous_datasets -.transform to general format.-> save_previous_data -save_previous_data --> compare - -compare -.Logs.-> save_log_report -download --> fetch -fetch --> validate -validate --> download -``` - -### Runner - -We use Apache Airflow for the Runner. - -### Source Spec - -This the specification of the Source object - -You can compare this to [CKAN Classic HarvestSource objects below](#harvest-source-objects). - -```javascript -id: -url: -title: -description: -date: -harvester_pipeline_id: // type in old CKAN -config: -enabled: // is this harvester enabled at the moment -owner: // user_id -publisher_id: // what is this?? Maybe the org the dataset is attached to ... -frequency: // MANUAL, DAILY etc -``` - -### UI - -* Jobs UI: moves to next gen -* Source UI: stays in classic for now ... - -### Installation - -CKAN Next Gen is in active development and is being deployed in production. - -You can find the code here: - -https://github.com/datopian/ckan-ng-harvester-core - -### Run it standalone - -TODO - -### How to integrate with CKAN Classic - -Config in CKAN MetaStore, ETL in new System - -* Keep storage and config in CKAN MetaStore (e.g. CKAN Classic) -* New ETL system for the actual harvesting process - -**Pulling Config** - -* Define a spec format for sources -* Script to convert this to Airflow DAGs -* Script to convert CKAN Harvest sources into the spec and hence into Airflow DAGs - -**Showing Status and Errors** - -* We create a viewer from Airflow status => JS SPA -* and then embed in CKAN Classic Admin UI - -```mermaid -graph LR - -config[Configuration] -etl[ETL - ckanext-harvesting etc] -ckan[CKAN Classic] -new["New ETL System"] - -subgraph "Current CKAN Classic" - config - etl -end - -subgraph Future - ckan - new -end - -config --> ckan -etl --> new -``` - -More detailed version: - -```mermaid -graph TD - -config[Configuration e.g. Harvest sources] -api[Web API - for config, status updates etc] -ui[User Interface to create harvests, see results] -metastore["Storage for the harvested metadata (+ data)"] -logging[Logging] - -subgraph "CKAN Classic" - config - ui - metastore - api - logging -end - -subgraph ETL - runner[Runner + Queue] - pipeline[Pipeline system] -end - - -pipeline --> metastore -config --> runner -runner --> pipeline -pipeline -.reporting/errors.-> logging -ui --> config -logging --> api -metastore --> api -config --> api -``` - -### How do I ... - -Support parent-child relationships in harvested datasets e.g. in data.json? - -Enhance / transform incoming datasets e.g. assigning topics based on sources e.g. this is geodata - - -## Appendix: CKAN Classic Harvesting in Detail - -https://github.com/ckan/ckanext-harvest - -README is excellent and def worth reading - key parts of that are also below. - -### Key Aspects - -* Redis and AMQP (does anyone use AMQP) -* Logs to database with API access (off by default) - https://github.com/ckan/ckanext-harvest#database-logger-configurationoptional -* Dataset name generation (to avoid overwriting) -* Send mail when harvesting fails -* CLI - https://github.com/ckan/ckanext-harvest#command-line-interface -* Authorization - https://github.com/ckan/ckanext-harvest#authorization -* Built in CKAN harvester - https://github.com/ckan/ckanext-harvest#the-ckan-harvester -* Running it: you run the queue listeners (gather ) - -Existing harvesters - -* CKAN - ckanext-harvest -* DCAT - https://github.com/ckan/ckanext-dcat/tree/master/ckanext/dcat/harvesters -* Spatial - https://github.com/ckan/ckanext-spatial/tree/master/ckanext/spatial/harvesters - - -### Domain model - -See https://github.com/ckan/ckanext-harvest/blob/master/ckanext/harvest/model/__init__.py - -* HarvestSource - a remote source for harvesting datasets from e.g. a CSW server or CKAN instance -* HarvestJob - a job to do the harvesting (done in 2 stages: gather and then fetch and import). This is basically state for the overall process of doing a harvest. -* HarvestObject - job to harvest one dataset. Also holds dataset on the remote instance (id / url) -* HarvestGatherError -* HarvestObjectError -* HarvestLog - -#### Harvest Source Objects - -https://github.com/ckan/ckanext-harvest/blob/master/ckanext/harvest/model/__init__.py#L230-L245 - -```python -# harvest_source_table -Column('id', types.UnicodeText, primary_key=True, default=make_uuid), -Column('url', types.UnicodeText, nullable=False), -Column('title', types.UnicodeText, default=u''), -Column('description', types.UnicodeText, default=u''), -Column('config', types.UnicodeText, default=u''), -Column('created', types.DateTime, default=datetime.datetime.utcnow), -Column('type', types.UnicodeText, nullable=False), -Column('active', types.Boolean, default=True), -Column('user_id', types.UnicodeText, default=u''), -Column('publisher_id', types.UnicodeText, default=u''), -Column('frequency', types.UnicodeText, default=u'MANUAL'), -Column('next_run', types.DateTime), # not needed -``` - -#### Harvest Error and Log Objects - -https://github.com/ckan/ckanext-harvest/blob/master/ckanext/harvest/model/__init__.py#L303-L331 - -```python -# New table -harvest_gather_error_table = Table( - 'harvest_gather_error', - metadata, - Column('id', types.UnicodeText, primary_key=True, default=make_uuid), - Column('harvest_job_id', types.UnicodeText, ForeignKey('harvest_job.id')), - Column('message', types.UnicodeText), - Column('created', types.DateTime, default=datetime.datetime.utcnow), -) -# New table -harvest_object_error_table = Table( - 'harvest_object_error', - metadata, - Column('id', types.UnicodeText, primary_key=True, default=make_uuid), - Column('harvest_object_id', types.UnicodeText, ForeignKey('harvest_object.id')), - Column('message', types.UnicodeText), - Column('stage', types.UnicodeText), - Column('line', types.Integer), - Column('created', types.DateTime, default=datetime.datetime.utcnow), -) -# Harvest Log table -harvest_log_table = Table( - 'harvest_log', - metadata, - Column('id', types.UnicodeText, primary_key=True, default=make_uuid), - Column('content', types.UnicodeText, nullable=False), - Column('level', types.Enum('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', name='log_level')), - Column('created', types.DateTime, default=datetime.datetime.utcnow), -) -``` - -### Key components - -* Configuration: HarvestSource objects -* Pipelines: Gather, Fetch, Import stages in a given harvest extension -* Runner: bespoke queue system (backed by Redis or AMQP). Scheduler is external e.g. Cron -* Logging: logged into CKAN DB (as Harvest Errors) -* Interface: API, Web UI and CLI -* Storage: - * Final: Datasets are in CKAN MetaStore - * Intermediate: HarvestObject in CKAN MetaStore - -### Flow - -0. *Harvest run:* a regular run of the Harvester that generates a HarvestJob object. This is then passed to gather stage. This is what is generated by cron `harvest run` execution (or from web UI) -1. The **gather** stage compiles all the resource identifiers that need to be fetched in the next stage (e.g. in a CSW server, it will perform a GetRecords operation). -2. The **fetch** stage gets the contents of the remote objects and stores them in the database (e.g. in a CSW server, it will perform an GetRecordById operations). -3. The **import** stage performs any necessary actions on the fetched resource (generally creating a CKAN package, but it can be anything the extension needs). - -```mermaid -graph TD - -gather[Gather] - -run[harvest run] --generates a HarvestJob--> gather -gather --generates HarvestObject for each remote source item --> fetch -fetch --HarvestObject updated with remote metadata--> import -import --store package--> ckan[CKAN Package] -import --HarvestObject updated --> harvestdb[Harvest DB] -``` - -```python -def gather_stage(self, harvest_job): - ''' - The gather stage will receive a HarvestJob object and will be - responsible for: - - gathering all the necessary objects to fetch on a later. - stage (e.g. for a CSW server, perform a GetRecords request) - - creating the necessary HarvestObjects in the database, specifying - the guid and a reference to its job. The HarvestObjects need a - reference date with the last modified date for the resource, this - may need to be set in a different stage depending on the type of - source. - - creating and storing any suitable HarvestGatherErrors that may - occur. - - returning a list with all the ids of the created HarvestObjects. - - to abort the harvest, create a HarvestGatherError and raise an - exception. Any created HarvestObjects will be deleted. - - :param harvest_job: HarvestJob object - :returns: A list of HarvestObject ids - ''' - -def fetch_stage(self, harvest_object): - ''' - The fetch stage will receive a HarvestObject object and will be - responsible for: - - getting the contents of the remote object (e.g. for a CSW server, - perform a GetRecordById request). - - saving the content in the provided HarvestObject. - - creating and storing any suitable HarvestObjectErrors that may - occur. - - returning True if everything is ok (ie the object should now be - imported), "unchanged" if the object didn't need harvesting after - all (ie no error, but don't continue to import stage) or False if - there were errors. - - :param harvest_object: HarvestObject object - :returns: True if successful, 'unchanged' if nothing to import after - all, False if not successful - ''' - -def import_stage(self, harvest_object): - ''' - The import stage will receive a HarvestObject object and will be - responsible for: - - performing any necessary action with the fetched object (e.g. - create, update or delete a CKAN package). - Note: if this stage creates or updates a package, a reference - to the package should be added to the HarvestObject. - - setting the HarvestObject.package (if there is one) - - setting the HarvestObject.current for this harvest: - - True if successfully created/updated - - False if successfully deleted - - setting HarvestObject.current to False for previous harvest - objects of this harvest source if the action was successful. - - creating and storing any suitable HarvestObjectErrors that may - occur. - - creating the HarvestObject - Package relation (if necessary) - - returning True if the action was done, "unchanged" if the object - didn't need harvesting after all or False if there were errors. - - NB You can run this stage repeatedly using 'paster harvest import'. - - :param harvest_object: HarvestObject object - :returns: True if the action was done, "unchanged" if the object didn't - need harvesting after all or False if there were errors. - ''' -``` - -### UI - -Harvest admin portal - -![](https://i.imgur.com/Y0cyrUG.png) - -Add a Harvest Source - -![](https://i.imgur.com/uoBFAjY.png) - -Clicking on Harvest Source gives you list of datasets harvested - -![](https://i.imgur.com/hIRrDHP.png) - -Clicking on about gives you.. - -![](https://i.imgur.com/S9GTl9n.png) - - -Admin view of a particular (Harvest) source - -![](https://i.imgur.com/Loeshhv.png) - - -Edit harvester - -![](https://i.imgur.com/3SOuCm5.png) - -Jobs summary - -![](https://i.imgur.com/QwD7nHi.png) - -Individual jobs - -![](https://i.imgur.com/ljKEk0l.png) diff --git a/site/content/docs/dms/hubstore.md b/site/content/docs/dms/hubstore.md deleted file mode 100644 index f50b42b7..00000000 --- a/site/content/docs/dms/hubstore.md +++ /dev/null @@ -1,31 +0,0 @@ -# HubStore - -> [!warning]Work in Progress -This is is early stage and still a work in progress. - -A HubStore maintains a catalog of organizations and their ownership of projects / datasets. - -It's name derives from the common appelation of "Hub" for something that organizes a collection of individual items e.g. Git*Hub* or Data*Hub*. The HubStore handles the information that makes a Hub a Hub. - -## Domain Model - -* Organization -* Account (User) -* MembershipRole e.g. admin, editor etc -* Project (which has a Dataset) - -Associations - -* Organization --owns--> Project -* Organization --membership--> Account - * Membership association has an associated MembershipRole. - -Potential extras: - -* Do we allow Accounts to own Projects or only Organizations? Yes, we do. I think this is a key use case. -* Organization Hierarchies: Organization --parent--> Organization. One could allow for hierarchies of organizations. We do not by default but it is possible to do so. -* Team: a convenient grouping of Accounts for the purpose of assigning permissions to something - * All team members have the same status (if you want different statuses get different teams) - * Team --membership--> Account (without a role) - * Example: Github Teams - * Comment: hirerarchical organizations *could* make much of the use case obsolete. IME teams are an annoying feature of github that bring complexity (who exactly has access to this thing, if i want to remove Person X from access i have to check all the teams with access etc). diff --git a/site/content/docs/dms/index.md b/site/content/docs/dms/index.md deleted file mode 100644 index 8fcd2594..00000000 --- a/site/content/docs/dms/index.md +++ /dev/null @@ -1,114 +0,0 @@ -
-

Datopian Tech

- - - - - -

- We are experts in data management and engineering - - This is an overview of our technology - -

-
- -## Data Management Systems - -A [Data Management System (DMS)][dms] is a _framework_. It can be used to create a variety of _solutions_ such as [Data Portals][], [Data Catalogs][], [Data Lakes][] (or Data Meshes) etc. We have developed two DMS stacks that share a set of underlying core components: - -- [CKAN][]: the open source data management system we created in 2007 and that we continue to develop and maintain. The main information on CKAN is at https://ckan.org/. Here we have some specific notes on how we develop and deploy CKAN as well as our thoughts on the [next generation of CKAN (v3)][v3]. -- [DataHub][]: a simpler version of CKAN focused on SaaS platform at DataHub.io. DataHub and CKAN v3 share many of the same core components. - -[data portals]: /docs/dms/data-portals -[data lakes]: /docs/dms/data-lake -[data catalogs]: /docs/dms/data-portals -[dms]: /docs/dms/dms -[CKAN]: /docs/dms/ckan - -### Solutions - -You can use a DMS to build many kinds of specific solutions - -- [Data Portals][portals] are gateways to data. That gateway can be big or small, open or restricted. For example, data.gov is open to everyone, whilst an enterprise "intra" data portal is restricted to its personnel. -- Data Catalog: see https://ckan.org/ -- Metadata manager: see [Publishing][] -- Data Lake: you can use a DMS to rapidly create a data lake using existing infrastructure. For example, using the DMS' catalog and storage gateway with existing cloud storage and data processing capabilities. -- Data Engineering: you can use components of the DMS to rapidly create, orchestrate and supply data pipelines. - -[dms]: /docs/dms/dms -[portals]: /docs/dms/data-portals -[publishing]: /docs/dms/publish -[datahub]: /docs/dms/datahub -[ckan]: /docs/dms/ckan -[v3]: /docs/dms/ckan-v3 - -### Features - -A DMS has a variety of features. This section provides an overview and links to specific feature pages that include details of how they work in CKAN and CKAN v3 / DataHub. - - - -> [!tip] There are many ways to break down features and this is just one framing. We are thinking about others and if you have thoughts please get in touch. - -- [Discovering and showcasing data (catalog and presenting)](/docs/dms/frontend) -- [Views on data](/docs/dms/views) including visualizing and previewing data as well [Data Explorers][explorer] and [Dashboards][] -- [Publishing data](/docs/dms/publish) -- [Data API DataStore](/docs/dms/data-api) -- [Permissions](/docs/dms/permissions) and [Authentication](/docs/dms/authentication) -- [Versioning](/docs/dms/versioning) -- [Harvesting](/docs/dms/harvesting) - -[dashboards]: /docs/dms/dashboards -[explorer]: /docs/dms/data-explorer - -### Components - -A DMS has the following key components: - -- [HubStore](/docs/dms/hubstore) -- [Data Flows and Factory](/docs/dms/flows) - - [Loading to DataStore](/docs/dms/load) -- [Storage](/docs/dms/storage) - - [Blob Storage](/docs/dms/blob-storage) - - [Structured Storage - see DataStore](/docs/dms/data-api) - -https://coggle.it/diagram/Xiw2ZmYss-ddJVuK/t/data-portal-feature-breakdown - - - -## Frictionless - -The Frictionless approach to data. See https://frictionlessdata.io/ - -Our team created this whilst at Open Knowledge Foundatioin and continue to co-steward it. - -## OpenSpending - -https://openspending.org/ - -## Developer Experience - -Service Reliability Engineering (SRE) and Developer Experience (DX) for our CKAN cluster technology. - -- [Developer Experience][dx] -- [DX - Deploy](/docs/dms/dx/deploy) -- [DX - Cluster](/docs/dms/dx/cluster) - -Old cluster - -- [Deploy in old cluster](/docs/dms/deploy) -- [Exporting from CKAN-Cloud](/docs/dms/migration) -- [Cloud](/docs/dms/cloud) - start on CKAN cloud documentation - -## Research - -- [Data Frames and what would a JS data frame library look like](/docs/dms/dataframe) -- [Dataset Relationships](/docs/dms/relationships) - -## Miscellaneous - -- [Glossary »](/docs/dms/glossary) -- [Notebook -- our informal blog »](/docs/dms/notebook) - -[dx]: /docs/dms/dx diff --git a/site/content/docs/dms/load.md b/site/content/docs/dms/load.md deleted file mode 100644 index d6fdc7c8..00000000 --- a/site/content/docs/dms/load.md +++ /dev/null @@ -1,183 +0,0 @@ -# Data Load - -## Introduction - -Data load covers functionality for automatedly loading structured data such as tables into a data management system. Data load is usually part of a larger [Data API (DataStore)][dapi] component. - -Load is distinct from uploading raw files ("blobs") and from a "write" data API: from blobs because the data is structured (e.g. rows and columns) and that structure is expected to be preserved; from a write data API because the data is imported in bulk (e.g. a whole CSV file) rather than writing one row at a time. - -The load terminology comes from ETL (extract, transform, load) though in reality this functionality will often include some extraction and transformation -- extracting the structured data from the source formats and potentially transformation if data needs some cleaning. - -[dapi]: /docs/dms/data-api - -### Features - -As a Publisher i want to load my dataset (resource) into the DataStore quickly and reliably so that my data is available over the data API. - -* Be “tolerant” where possible of bad data so that it still loads -* Get feedback on load progress, especially if something went wrong (with info on how I can fix it), so that I know my data is loaded (or if not what I can do about it) -* I want to update the schema for the data so that the data has right types (before and/or after load) -* I want to be able to update with a new resource file and only have it load the most recent - -For sysadmins: - -* Track Performance: As a Datopian Cloud Sysadmin I want to know if there are issues so that I can promptly address any problems for clients -* One Data Load Service per Cloud: As a Datopian Cloud Manager I may want to have one “DataLoad” service I maintain rather than one per instance for efficiency … - -### Flows - -#### Automatic load - -* Users uploads a file to portal using the Dataset editor - * This is stored into the blob storage (i.e. local or cloud storage) -* A "PUSH" notification is triggered to loader service -* Loader service load file to data API backend (a structured database with web API) - -```mermaid -sequenceDiagram - participant a as User - participant b as Blob Storage - participant c as CKAN - participant d as Loader - participant e as DataStore - - a->>c: create a resource with a location of remote file - c->>d: push notification - d->>b: pull it - d->>e: push it - d-->>c: success (or failure) notification -``` - -#### Sequence diagram for manual load - -The load to the data API system can also be triggered manually: - -```mermaid -sequenceDiagram - participant a as User - participant b as Blob Storage - participant c as CKAN - participant d as Loader - participant e as DataStore - - a->>c: click on upload button - c->>d: push notification - d->>b: pull it - d->>e: push it - d-->>c: success (or failure) notification -``` - -## CKAN v2 - -The actual loading is provided by either DataPusher or XLoader. There is a common UI. There is no explicit API to trigger a load -- instead it is implicitly triggered, for example when a Resource is updated. - -### UI - -The UI shows runs of the data loading system with information on success or failure. It also allows eidtors to manually retrigger a load. - -![](https://i.imgur.com/fSh2cwK.png) - -TODO: add more screenshots - -### DataPusher - -Service (API) For pushing tabular data to datastore. Do not confuse it with `ckanext/datapusher` in ckan core codbase which is simply an extension communicating with the DataPusher API. DataPusher itself is a standalone service, running separately from CKAN app. - -https://github.com/ckan/datapusher - -https://docs.ckan.org/projects/datapusher/en/latest/ - -https://docs.ckan.org/en/2.8/maintaining/datastore.html#datapusher-automatically-add-data-to-the-datastore - -### XLoader - -XLoader runs as async jobs within CKAN and bulk loads data via Postgres COPY command. This is fast but it does mean it only loads data as strings and explicit type-casting must be done after the load (the user must edit the data dictionary). XLoader was built to address 2 major issues with DataPusher: - -* Speed: DataPusher converts data row by row and writes over the DataStore write API and hence is quite slow. -* Dirty data: DataPusher attempts to guess data types and then cast and this regularly led to failures which though logical were frustrating to users. XLoader gets the data in (as strings) and let's the user sort out types later. - -https://github.com/ckan/ckanext-xloader - -* `load_csv`: https://github.com/ckan/ckanext-xloader/blob/master/ckanext/xloader/loader.py#L40 -* Loader: https://github.com/ckan/ckanext-xloader/blob/master/ckanext/xloader/jobs.py#L100 - -How does the queue system work: job queue is done by RQ, which is simpler and is backed by Redis and allows access to the CKAN model. Job results are currently still stored in its own database, but the intention is to move this relatively small amount of data into CKAN's database, to reduce the complication of install. - -### Flow of Data in Data Load - -```mermaid -graph TD - -datastore[Datastore API] -datapusher[DataPusher] -pg[Postgres DB] -filestore[File Store] -xloader[XLoader] - -filestore --"Tabular data"--> datapusher -datapusher --> datastore -datastore --> pg - -filestore -. or via .-> xloader -xloader --> pg -``` - -Sequence diagram showing the journey of a tabular file into the DataStore: - -```mermaid -sequenceDiagram - Publisher->>CKAN Instance: Create a resource (or edit existing one) in a dataset - Publisher->>CKAN Instance: Add tabular resource from disk or URL - CKAN Instance-->>FileStore: Upload data to storage - CKAN Instance-->>Datapusher: Add job to queue - Datapusher-->>Datastore: Run the job - push data via API - Datastore-->>Postgres: Create table per resource and insert data -``` - -### FAQs - -Q: What happens with non-tabular data? -A: CKAN has a list of types of data it can process into the DataStore (TODO:link) and will only process those. - -### What Issues are there? - -Generally: the Data Load system is an hand-crafted, bespoke mini-ETL process. It would seem better to use high-quality third-party ETL tooling here rather than hand-roll be that for pipeline creation, monitoring, orchestration etc. - -Specific examples: - -* No connection between DataStore system and CKAN validation extension powered by GoodTables https://github.com/frictionlessdata/ckanext-validation Thus, for example, users may edit the DataStore Data Dictionary and be confused that this has no impact on validation. More generally, data validation and data loading might naturally be part of one overall ETL process but Data Load system is not architected in a way that makes this easy to add. -* No support for Frictionless Data spec sand their ability to specific incoming data structure (CSV format, encoding, column types etc). - * Dependent on error-prone guessing of types or manual type conversion - * Makes it painful to integrate with broader data processing pipeline (e.g. clean separation would allow type guessing to be optimized elsewhere in another part of the ETL pipeline) -* Excel loading won't work or won't load all sheets -* DataPusher - * https://github.com/ckan/ckanext-xloader#key-differences-from-datapusher - * Works terribly with loading a bit big data. It may for no reason crash after hour of loading. And after reload it goes along - * Is slow esp for large datasets and even smallish datasets e.g. 25Mb - * often fails due to e.g. data validation/casting errors but this not clear (and unsatisfying to the user) -* XLoader: - * Doesn't work with XLS(X) - * has problems fetching resources from Blob Storage (it fails and need to wait until the Resource is uploaded.) - * raising Exception NotFound when CKAN has a delay creating resources - * re-submits Resources when creating a new Resource - * XLoader sets `datastore_active` before data is uploaded - - -## CKAN v3 - -The v3 implementation is named 💨🥫 AirCan: https://github.com/datopian/aircan - -Its a lightweight, standalone service using AirFlow. - -Status: Beta (June 2020) - -* Runs as a separate microservice with zero coupling with CKAN core (=> gives cleaner separation and testing) -* Uses Frictionless Data patterns and specs where possible e.g. Table Schema for describing or inferring the data schema -* Uses AirFlow as the runner -* Uses common ETL / [Data Flows][] patterns and frameworks - -[Data Flows]: /docs/dms/flows - -### Design - -See [Design page »](/docs/dms/load/design/) diff --git a/site/content/docs/dms/load/design.md b/site/content/docs/dms/load/design.md deleted file mode 100644 index f7874619..00000000 --- a/site/content/docs/dms/load/design.md +++ /dev/null @@ -1,183 +0,0 @@ -# Data Load Design - -Key point: this is classic ETL so let's reuse those patterns and tooling. - -## Logic - -```mermaid -graph LR - -usercsv[User has CSV,XLS etc] -userdr[User has Tabular Data Resource] -dr[Tabular Data Resource] - -usercsv --1. some steps--> dr -userdr -. direct .-> dr -dr --2. load --> datastore[DataStore] -``` - -In more detail, dividing ET(transform) from L(oad): - -```mermaid -graph TD - -subgraph "Prepare (ET)" - rawcsv[Raw CSV] --> tidy[Tidy] - tidy --> infer[Infer types] - infer -end - -infer --> tdr{{Tabular Data Resource
csv/json + table schema}} -tdr --> dsdelete - -subgraph "Loader (L)" - datastorecreate[DataStore Create] - dsdelete[DataStore Delete] - load[Load to CKAN via DataStore API or direct copy] - - dsdelete --> datastorecreate - datastorecreate --> load -end -``` - -### Load step in even more detail - -```mermaid -graph TD - -tdr[Tabular Data Resource on disk from CSV in FileStore of a resource] -loadtdr[Load Tabular Data Resource Metadata] - -dscreate[Create Table in DS if not exists] -cleartable[Clear DS table if existing content] -pushdatacopy[Load to DS via PG copy] -done[Data in DataStore] - -tdr --> loadtdr -loadtdr --> dscreate -dscreate --> cleartable - -cleartable --> pushdatacopy - -pushdatacopy --> done - -logstore[LogStore] - -cleartable -. log .-> logstore -pushdatacopy -. log .-> logstore -``` - -## Runner - -We will use AirFlow. - - -## Research - -### What is a Tabular Data Resource? - -See Frictionless Specs. For our purposes: - -* A "Good" CSV file: Valid CSV - with one header row, No blank header etc... -* Encoding worked out -- usually we should have already converted to utf-8 -* Dialect - https://frictionlessdata.io/specs/csv-dialect/ -* Table Schema https://frictionlessdata.io/specs/table-schema - -NB: even if you want to go direct loading route (a la XLoader) and forget types you still need encoding etc sorted -- and it still fits in diagram above (Table Schema is just trivial -- everything is strings). - -### What is datastore and how to create the DataStore entry - -https://github.com/ckan/ckan/tree/master/ckanext/datastore -* provides an ad hoc database for storage of structured data from CKAN resources -* Connection with Datapusher: https://docs.ckan.org/en/2.8/maintaining/datastore.html#datapusher-automatically-add-data-to-the-datastore -* Datastore API: https://docs.ckan.org/en/2.8/maintaining/datastore.html#the-datastore-api - * Making Datastore API requests: https://docs.ckan.org/en/2.8/maintaining/datastore.html#making-a-datastore-api-request - -#### Create an entry - -``` -curl -X POST http://127.0.0.1:5000/api/3/action/datastore_create -H "Authorization: {YOUR-API-KEY}" - -resource --d '{ - "resource": {"package_id": "{PACKAGE-ID}"}, - "fields": [ {"id": "a"}, {"id": "b"} ] - }' -``` - -https://docs.ckan.org/en/2.8/maintaining/datastore.html#ckanext.datastore.logic.action.datastore_create - -### Options for Loading - -There are 3 different paths we could take: - -```mermaid -graph TD - -pyloadstr[Load in python in streaming mode] -cleartable[Clear DS table if existing content] -pushdatacopy[Load to DS via PG copy] -pushdatads[Load to DS via DataStore API] -pushdatasql[Load to DS via sql over PG api] -done[Data in DataStore] -dataflows[DataFlows SQL loader] - -cleartable -- 1 --> pyloadstr -pyloadstr --> pushdatads -pyloadstr --> pushdatasql -cleartable -- 2 --> dataflows -dataflows --> pushdatasql -cleartable -- 3 --> pushdatacopy - -pushdatasql --> done -pushdatacopy --> done -pushdatads --> done -``` - -#### Pros and Cons of different approaches - -|Criteria | Datastore Write API | PG Copy | Dataflows | -|---------|:--------- |:------- | ---------: | -| Speed | Low | High | ??? | -|Error Reporting| Yes | Yes | No(?) | -|Easy of implementation| Yes | No(?) | Yes | -Works Big data| No | Yes | Yes(?) | -|Works well in parrallel| No | Yes(?) | Yes(?) - -### DataFlows - -https://github.com/datahq/dataflows - -Dataflows is a framework for loading, processing, manipulating data. - -* Loader (Loading from external source (or disk)): https://github.com/datahq/dataflows/blob/master/dataflows/processors/load.py -* Load to an SQL db (Dump processed data) https://github.com/datahq/dataflows/blob/master/dataflows/processors/dumpers/to_sql.py -* What is error reporting, what is runner system ..., does it have a UI? does it have a queue system? - * Think data package pipelines is taking care of all of these. https://github.com/frictionlessdata/datapackage-pipelines - * DPP itself is also a ETL framework, just much heavier and a bit complicated. - -### Notes an QA (Sep 2019) - -* Note: TDR needs info on CKAN Resource source so we can create right datastore entry .. -* No need to validate as we assume it is good ... - * We might want to do that ... still -* Pros and Cons - * Speed - * Error reporting ... - * What happens with Copy if you hit an error (e.g. a bad cast?) - * https://infinum.co/the-capsized-eight/superfast-csv-imports-using-postgresqls-copy - * https://wiki.postgresql.org/wiki/Error_logging_in_COPY - * Ease of implementation - * Good with inserting Big data -* Create as strings and cast later ... ? -* xloader implementation with COPY command: https://github.com/ckan/ckanext-xloader/blob/fb17763fc7726084f67f6ebd640809ecc055b3a2/ckanext/xloader/loader.py#L40 - -Raw insert ~ 15m (on 1m rows) -Insert with begin / commit ~5m -copy ~82s (though may have limit on b/w) -- and what happens if pipe breaks - -Q: Is it better to but everything in DB as a string and cast later or cast and insert in DB. -A: Probably cast first and insert after. - -Q: Why do we rush to insert the data in DB? We will have to wait until it's casted anyways befroe use -A: It's much faster to do operations id DB than outside. diff --git a/site/content/docs/dms/notebook/index.md b/site/content/docs/dms/notebook/index.md deleted file mode 100644 index 3344d3a7..00000000 --- a/site/content/docs/dms/notebook/index.md +++ /dev/null @@ -1,593 +0,0 @@ -# Notebook - -Our lab notebook. Informal thoughts. A very raw blog. - -# Data Literate - a small Product Idea 2021-05-17 @rufuspollock - -I want to write a README with data and vis in it and preview it ... - -* Markdown is becoming a lingua franca for writing developer and even research docs - * It's quick and ascii-like - * It's widely supported - * It's extensible ... -* Frontend tooling is rapidly evolving ... - * The distant between code and a tool is declining => I might as well write code ... -* MDX = Markdown + react -* RStudio did this a while ago ... -* Missing part is data ... - * You have juputer notebooks etc ... => they are quite high end ... - -``` -Notebooks (jupyter, literate programming) ==> - Write text and code together - Write code like in a terminal - Data oriented -``` - -Visualization - -React - -Markdown ... - ---- - -Here the kinds of doc i want to write - -``` -## A Dataset - -\``` -# Global Solar Supply (Annual) - -Solar energy supply globally. - -Source: International Energy Association https://www.iea.org/reports/solar-pv. - -| Year | Generation (TWh) | % of total energy | -|--|--| -|2008|12| -|2009|20| -|2010|32| -|2011|63| -|2012|99| -|2013|139| -|2014|190| -|2015|251| -|2016|329| -|2017|444| -|2018|585| -|2019|720| 2.7 | -\``` - -Europe Brent Spot Prices (Annual/ Monthly/ Weekly/ Daily) from U.S. Energy Information Administration (EIA). - -Source: https://www.eia.gov/dnav/pet/hist/RBRTEd.htm -``` - -### Notes - -R Markdown - https://rmarkdown.rstudio.com - -> Use a productive notebook interface to weave together narrative text and code to produce elegantly formatted output. - - -## A DMS is a tool, a Data Portal is a solution 2021-03-14 @rufuspollock - -Over the years, we've seen many different terms used to describe software like CKAN and the solutions it is used to create e.g. data catalog, data portal, data management system, data platform etc. - -Over time, personally, I've converged towards [data management system (DMS)](/docs/dms/dms) and [data portal](/docs/dms/data-portals). But I've still got two terms and even people in my own team ask me to clarify what the difference is. Recently it became clear to me: - -**A data management system (DMS) is a tool. A data portal is a solution.** - -And a data management system is a tool you can use to build a data portal. Just like you can use a hammer to build a house. - -## Data Factory Concepts 2020-09-03 @rufuspollock - -Had this conceptual diagram hanging round for a couple of years. - -``` -Objects - -Row - File - Dataset - -Transformations - -Operator - Pipeline - Flow -```` - -NTS - -* A factory could be a (DA)G of flows b/c could be dependencies between them ... e.g. run ComputeKeyMetrics only after all other flows have run ... -* But not always like that: flows can be completely independent. - -## Current Data Factory Components (early 2019) - -``` - Factory - Runners, SaaS platform etc - -datapackage-pipelines -> (dataflows-server / dataflows-runner) -dataflows-cli : generators, hello-world, 'init', 'run' -goodtables.io -"blueprints": standard setups for a factory (auto-guessed?) - - DataFlows: Flow Libs - -dataflows : processor definition and chaining -processors-library: stdlib, user contributed items [dataflows-load-csv] - - Data Package Libs - -data.py, DataPackage-py, GoodTables, ... -tabulator, tableschema -``` - -## Composition vs Inheritance approaches to building applications and esp web apps 2020-08-20 @rufuspollock - -tl;dr: composition is better than inheritance but many systems are built with inheritance - -Imagine we want a page like this: - -``` -
-{{title}} - -``` - -Inheritance / Slot model - -``` -def render_home: - return render('base_template.html'< { - title="hello world" - }) -``` - -Composition / declarative - -``` -def render_home: - mytitle = 'hello world' - response.write(get_header()) - response.write(mytitle) - response.write(get_footer() -``` - -You can write templates two ways: - -### Inheritance - -Base template - -```html -# base.html -
-{{title}} - -{{content}} - -``` - -`blog-post.html` - -``` -{%extends base.html %} -{{title}} -- Blog -``` - -### Composition - -`blog-post.html` - -``` - - -{{content}} - - -``` - - - -## How Views should work in CKAN v3 (Next Gen) 2020-08-10 @rufuspollock - -Two key points: - -1. Views should become an explicit (data) project level object -2. Previews: should be very simple and work *without* data API - -Why? - -* Views should become an explicit (data) project level object - * So I can show a view on dataset page (atm I can't) - * I have multiview view inside reclinejs but rest are single views ... (this is confusing) - * I can't create views across multiple resources - * They are nested under resources but they aren't really part of a resource -* Previews: should be very simple and work *without* data API - * so they work with revisions (atm views often depend on data API which causes problems with revisions and viewing old revisions of resources) - -Distinguish 3 concepts - -* Preview: a very simple method for previewing specific raw data types e.g. csv, excel, json, xml, text, geojson etc ... - * Key aspect are ability to sample a part and to present. -* Viz: graph, map, ... (visualizations) -* Query UI: UI for creating queries -* Viz Builder: a UI for creating charts, tables, maps etc -* Explorer (dashboard): combines query UI, Viz Builder and Viz Renderer - -## What a (future) Data Project looks like - -NB: To understand what a project is see [DMS](/docs/dms/dms). - -It helps me to be very concreate and imagine what this looks looks like on disk: - -``` -datapackage.json # dataset -data.csv # dataset resources (could be anywhere) -views.yml -data-api.yml -flows.yml -``` - -Or, a bit more elegantly: - -``` -data/ - gdp.csv | gdp.pdf | ... -views/ - graph.json | table.json | ... -api/ - gdp.json | gdp-ppp.json | ... -flows/ - ... -README.md -datapackage.json # ? does this just contain resources or more than that? Just resources -``` - -## Data Factory 2020-07-23 @rufuspollock - -I've used the term Data Factory but it's not in common use. At the same time, there doesn't seem to be a good term out there in common use for what I'm referring to. - -What am I referring to? - -I can start with terms we do have decent-ish terminlogy for: data pipelines or data flows. - -Idea is reasonably simple: I'm doing stuff to some data and it involves a series of processing steps which I want to run in order. It may get more complex: rather than a linear sequence of tasks I may have branching and/or parallelization. - -Because data "flows" through these steps from one end to the otehr we end up with terminology like "flow" or "pipeline". - -Broken down into its components we have two things: - -* Tasks: the individual processing nodes, i.e. a single operator on a unit of data (aka Processors / Operators) -* Pipeline: which combines these tasks into a whole (aka Flow, Graph, DAG ...). It is a DAG (directed acycle graph) of processors starting from one or more sources and ending in one or more sinks. Note the simple case of a linear flow is very common. - -[NB: tasks in an actual flow could either be bespoke to that flow or are configured instances of a template / abstract tasks e.g. load from s3 might be be a template task which as a live task in an actual flow will be configured with a specific bucket.] - -So far, so good. - -But what is the name for a system (like AirFlow, Luigi) for creating, managing and running data pipelines? - -My answer: a Data Factory. - -What I don't like with this is that it messes with the metaphor: factories are different from pipelines. If we went with Data Factory then we should talk about "assembly lines" rather than "pipelines" and "workers" (?) rather than tasks. If one stuck with water we'd end up with something like a Data Plant but that sounds weird. - -Analogy: for data we clearly have a file and dataset. And a system for organizing and managing datasets is a data catalog or data management system. So what's the name for the system for processing datasets ... - -And finally :checkered_flag: I should mention [AirCan][], our own Data Factory system we've been developing built on AirFlow. - -[AirCan]: https://github.com/datopian/aircan/ - -### Appendix: Terminology match up - -| Concept | Airflow | Luigi | -|---------|----------|-------| -| Processor | Task | Task | -| Pipeline | DAG | ? | -| Pipeline (complex, branching) | DAG | - - -## Commonalities of Harvesting and Data(Store) Loading 2020-06-01 @rufuspollock - -tags: portal, load, factory - -Harvesting and data loading (for data API) are almost identical as mechanisms. As such, they can share the same "data factory" system. - -Data API load to backing DB (CKAN DataStore + DataPusher stuff) - -```mermaid -graph LR - -subgraph Factory - read --> process - process --> load - orchestrator -end -load --> db[DB = DataStore] - -orchestrator --> api -api --> wui[Dataset Admin UI] -``` - -Harvesting - -```mermaid -graph LR - -subgraph Factory - read --> process - process --> load[Load - sync with CKAN] - orchestrator -end -load --> db[DB = MetaStore] - -orchestrator --> api -api --> wui[Harvesting Admin UI] -``` - -## 10 things I regret about NodeJS by Ryan Dahl (2018) 2020-05-17 @rufuspollock - -Valuable generally and more great lessons for data packaging. - -https://www.youtube.com/watch?v=M3BM9TB-8yA - -### package.json and npm were a mistake - -Why package.json was a mistake: https://youtu.be/M3BM9TB-8yA?t=595 - -![](https://i.imgur.com/Ia0qtVm.png) - -* `npm` + a centralized repo are unnecessary (and were a mistake) -* doesn't like centralized npm repo (I agree) and look what go are doing. Sure, you probably have something via the backdoor at the end (e.g. go is getting that) for caching and reliability purposes, but it is not strictly necessary. - -ASIDE: It's the core tool (node) that make a metadata format and packager relevant and successful. It's node require allowing using of `package.json` or bundling `npm` in by defaultonly - -* Kind of obvious when you think about it -* Something i've always said re Data Packages (but not strongly enough and not followed enough): the tooling comes first and the format is, in many ways, a secondary conveinience. To be fair to myself (and others) we did write `dpm` first (in 2007!), and do a lot of stuff with the workflow and toolchain but its easy to get distracted. - -![](https://i.imgur.com/5E0Hffs.png) - -* modules aren't strictly necessary and `package.json` metadata is not needed -* on the web you just have js file and you can include them ... -* "package.json has all this unnecessary noise in it. like license, repository. Why am i filling this out. I feel like a book-keeper or something. This is just unnecessary stuff *to do* when all I am trying to do is link to a library" [ed: **I think this is a major relevance for Data Packages. There's a tension between the book-keepers who want lots of metadata for publishing etc ... and ... the data science and data engineers who just want to get something done. If I had a choice (and I do!) I would prioritize the latter massively. And they just care about stuff like a) table schema b) get me the data on my hard disk fast**] -* "If only relative files and URLs were used when importing, the path defines the version. There is no need to list dependencies" [ed: cf Go that did this right] - * And he's borrowed from Go for deno.land - -### Vendoring by default with `node_modules` was a mistake - -Vendoring by default with `node_modules` was a mistake - just use an env variable `$NODE_PATH` - -* `node_modules` then becomes massive -* module resolution is (needlessly) complex - -### General point: KISS / YAGNI - -E.g. `index.js`was "cute" but unnecessary, allowing `require xxx` without an extension (e.g. `.js` or `.ts` ) means you have to probe the file system. - -+data package. +data packaging. +frictionless. +lessons - -## Go modules and dependency management (re data package management) 2020-05-16 @rufuspollock - -Generally Go does stuff well. They also punted on dependency management initially. First, you just installed a url it was up to you to manage your depedencies. Then there was a period of chaos as several package/dependency managers fought it out (GoDeps etc). Then, ~ 2018 the community came together led by Russ Cox and came up with a very solid proposal which is official as of 2019. - -Go's approach to module (package) and dependency management can be an inspiration for Frictionless and Data Packages. Just as we learnt and borrowed a lot from Python and Node so we can learn and borrow from Go. - -1. Overview (by Russ Cox the author): https://research.swtch.com/vgo -2. The Principles of Versioning in Go https://research.swtch.com/vgo-principles -3. A Tour of Versioned Go (vgo) https://research.swtch.com/vgo-tour -4. cmd/go: add package version support to Go toolchain https://github.com/golang/go/issues/24301 -5. Using Go Modules - https://blog.golang.org/using-go-modules (official introduction on go blog) -6. Publishing Go Modules https://blog.golang.org/publishing-go-modules -7. Main wiki article and overview https://github.com/golang/go/wiki/Modules - -### Key principles - -> These are the three principles of versioning in Go, the reasons that the design of Go modules deviates from the design of Dep, Cargo, Bundler, and others. -> -> 1. Compatibility. The meaning of a name in a program should not change over time. -> 2. Repeatability. The result of a build of a given version of a package should not change over time. https://research.swtch.com/vgo-principles#repeatability -> 3. Cooperation. To maintain the Go package ecosystem, we must all work together. Tools cannot work around a lack of cooperation. - -Summary - -* Go used urls for identifiers for packages (including special cases for github) - * e.g. `import rsc.io/quote` - * Brilliant! No more dependency resolution via some central service. Just use the internet. -* Go installed packages via `go get` e.g. `go get rsc.io/quote`. This would install the module into `$GOPATH` at `rsc.io/quote` - * They did the absolute minimum: grab the files onto your hard disk under `$GOPATH/src`. `import` would then search this (IIUC) -* There was no way originally to get a version but with go modules (go > 1.11) you could do `go get rsc.io/quote@[version]` -* Dependency management is actually complex: satisfying dependency requirements is NP complete. Solve this by ... - * The Node/Bundler/Cargo/Dep approach is one answer. Allow authors to specify arbitrary constraints on their dependencies. Build a given target by collecting all its dependencies recursively and finding a configuration satisfying all those constraints. => SAT solver => this is complex. - * Go has a different solution - * Versioning is central to dependency management => you need to get really clear on versioning. Establish a community rule that you can only break compatibility with major versions ... - * Put breaking version (e.g. major versions) **into the url** so that you actually have a different package ... - - > For Go modules, we gave this old advice a new name, the import compatibility rule: - - >> If an old package and a new package have the same import path, - >> the new package must be backwards compatible with the old package. - - - ![](https://research.swtch.com/impver@2x.png) - - * Install the minimal version of a package that satisfies the requirements (rather than the maximal version) => this yields repeatability (principle 2) - * In summary Go differs in that: all versions are explicit (no `<=`, `>=`). Since we can assume that all later versions of a module are backwards compatible (and any breaking change generates a new module with explicit `vX` in name) we can simply cycle through a module and its dependencies and find the highest version that are listed and install that. -* Publishing a module is just pushing to github/gitlab or putting it somewhere on the web -- see https://blog.golang.org/publishing-go-modules -* Tagging versions is done with git tag -* "A module is a collection of related Go packages that are versioned together as a single unit." - -Layout on disk in a module (see e.g. https://blog.golang.org/publishing-go-modules). Main file `go.mod` and one extra for storing hashes for verification (it's not a lock file) - -``` -$ cat go.mod -module example.com/hello - -go 1.12 - -require rsc.io/quote/v3 v3.1.0 - -$ cat go.sum -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -``` - - -### Asides - -#### Vendoring is an incomplete solution to package versioning problem - -> More fundamentally, vendoring is an incomplete solution to the package versioning problem. It only provides reproducible builds. It does nothing to help understand package versions and decide which version of a package to use. Package managers like glide and dep add the concept of versioning onto Go builds implicitly, without direct toolchain support, by setting up the vendor directory a certain way. As a result, the many tools in the Go ecosystem cannot be made properly aware of versions. It's clear that Go needs direct toolchain support for package versions. https://research.swtch.com/vgo-intro - -## 2020-05-16 @rufuspollock - -Ruthlessly retain compatibility after v1 - inspiration from Go for Frictionless - -> It is intended that programs written to the Go 1 specification will continue to compile and run correctly, unchanged, over the lifetime of that specification. Go programs that work today should continue to work even as future “point” releases of Go 1 arise (Go 1.1, Go 1.2, etc.). -> -> — https://golang.org/doc/go1compat - -And they go further -- not just Go but also Go packages: - -> Packages intended for public use should try to maintain backwards compatibility as they evolve. The Go 1 compatibility guidelines are a good reference here: don’t remove exported names, encourage tagged composite literals, and so on. If different functionality is required, add a new name instead of changing an old one. If a complete break is required, create a new package with a new import path. -> -> The Go FAQ has since Go 1.2 in November 2013 - -+frictionless - -## 2020-05-15 @rufuspollock - -`Project` should be the primary object in a DataHub/Data Portal -- not Dataset. - -Why? Because actually this is more than a Dataset. For example, it includes issues or workflows. A project is a good name for this that is both generic and specific. - -cf Git-hub e.g. Gitlab (and Github). Gitlab came later and did this right: it's primary object is a Project which hasA Repository. Github still insits on calling them repositories (see primary menu item which is "Create a new repository"). This is weird, a github "repository" isn't actually a github repository: it has issues, stats, workflows and even a discussion board now. Calling it a project is the accurate description and the repository label is a historical artifact when that was all it was. I sometimes create "repos" on Github just to have an issue tracker. Gitlab understands this and actually allows me to have projects without any associated repository. - -TODO: take a screenshot to illustrate Gitlab and Github. - -+flashes of insight. +datahub +data portal. +domain model. - -## 2020-04-23 @rufuspollock - -4 Stores of a DataHub - ->[!tip]Naming is one of the most important things -- and hardest! - -* MetaStore [service]: API (and store) of the metadata for datasets -* HubStore [service]: API for registry of datasets (+ potentially organizations and ownership relationships to datasets) -* BlobStore [service]: API for blobs of data -* StructuredStore [service]: API for structured data - -Origins: - -* Started using MetaStore in DataHub.io back in 2016 -* Not used in CKAN v2 -* Conceptually CKAN originally was MetaStore and HubStore. - -In CKAN v2: - -* MetaStore and HubStore (no explicit name) => main Postgres DB -* StructuredStore (called DataStore) => another separate Postgres DB -* BlobStore (called FileStore) => local disk (or cloud with an extension) - -In CKAN v3: propose to separate these explicitly ... - -## Data Portal vs DataHub vs Data OS 2020-04-23 @rufuspollock - -Data Portal vs DataHub vs Data OS -- naming and definitions. - -Is a Data Portal a DataHub? Is a DataHub a DataOS? If not, what are the differences? - -+todo - -## Data Concepts - from Atoms to Organisms 2020-03-05 @rufuspollock - -``` -Point -> Line -> Plane (0d -> 1d -> 2d -> 3d) - -Atom -> Molecule -> Cell -> Organism -``` - -```mermaid -graph TD - -cell[Cell] -row[Row] -column[Column] -table["Table (Sheet)"] -cube["Database (Dataset)"] - -cell --> row -cell --> column -row --> table -column --> table -table --> cube -``` - -``` - Domain => -Dimension - | - V -``` - -| Dimension | ... | Math | Spreadsheets | Databases | Tables etc | Frictionless | Pandas | R | -|--|--|--|--|--|--|--|--|--| -| 0d | Datum | Value | Cell | Value | Scalar? | N/A | Value | ? | -| 1d | .... | Array / Vector | Row | Row | Row | N/A | -| 1d | .... | N/A | Column | -| 2d | Grid? | Matrix | Sheet | Table | Table | TableSchema | -| 3d | Cube | 3d Matrix | Spreadsheet | Database (or Cube) | N/A | ? | ? | ? | -| 4d+ | HyperCube | n-d Matrix | - -What's crucial about a table is that it is not just an array of arrays or a rowset but a rowset plus a fieldset. - -``` -Field => FieldSet -Row => RowSet -``` - -A Table is a FieldSet x RowSet (+ other information) - -There is the question of whether there is some kind of connection or commonality at each dimension up ... e.g. you could have an array of arrays where each array has different fields ... - -```json -{ "first": "joe", "height": 3 } -{ "last": "gautama", "weight": 50 } -``` - -But a table has common fields. - -```json -{ "first": "joe", "height": 3 } -{ "first": "siddarta", "height": 50 } -``` - -(NB: one could always force a group of rows with disparate fields into being a table by creating the union of all the fields but that's hacky) - -So a table is a RowSet plus a FieldSet where each row conforms to that FieldSet. - -Similarly, we can aggregate tables. By default here the tables do *not* share any commonality -- sheets in a spreadsheet need not share any common aspect, nor do tables in a database. If they do, then we have a cube. - -NEXT: moving to flows / processes. - -## Is there room / need for a simple dataflow library? 2020-02-23 @rufuspollock - -Is there room / need for a simple dataflow library ...? - -What kind of library? - -* So Apache Beam /Google DataFlow is great ... and it is pretty complex and designed for use out of the box in parallel etc -* Apache Nifi: got a nice UI, Java and heavy duty. -* My own experience (even just now with key metrics) is i want something that will load and pipe data between processors - -Ideas - -* create-react-app for data flows: quickly scaffold data flows -* What is the default runner diff --git a/site/content/docs/dms/permissions.md b/site/content/docs/dms/permissions.md deleted file mode 100644 index a2eb0cc5..00000000 --- a/site/content/docs/dms/permissions.md +++ /dev/null @@ -1,60 +0,0 @@ -# Permissions (Authorization) - -As a Data Portal Owner I want only authenticated and authorized users to view (e.g. my staff), to edit (specific groups) so that we can put data in the portal and know that only appropriate people can use and contribute - -* Access to data is only to those we have authorized (and we don't give access to public or competitors unless we choose to!) -* we don't disclose information inappropriately internally (e.g. info with privacy restrictions) -* People don't accidentally edit others datasets - -Permissions breaks down into two parts: - -* Authentication: who are you? -* Authorization: what can you do? => much bigger - -## Authorization - -As a Dataset Owner I want to be able to limit access, editing etc to my datasets at several levels and using org/teams and potentially other mechanisms so that I can easily comply with PII restrictions whilst making my data as widely available as possible and enabling collaborators to contribute easily - -### Differentiating Metadata and Data Access - -As a Dataset Owner I want to allow viewing of the dataset metadata including the list of resources whilst limiting access to the data itself (e.g. restricting download) so that I can allow others to discover the data i have (and request access) whilst complying with restrictions on data access (e.g. PII) - -* TODO: what about *pre*viewing? - -### Editing Controls - -As a Dataset Owner I want to restrict those who can edit my dataset so that only those I authorize can edit the dataset - -* I probably want to do this in bulk e.g. add my whole organization/team - -### Update Permissions - -As a Dataset Owner I want to control who has the ability to change permisssions on my dataset so that only people i choose can do this ... - -* Default would be e.g. Dataset Owner + Org Admin can do this ... -* Are other options desired / possible? - -### Private Datasets - -As a Dataset Owner I want to make a dataset "private" so that it is only visible to those who have "edit" access on the dataset and is invisible to everyone else - -### Adding one-off collaborators - -As a Dataset Owner I want to add someone outside of my organization to a restricted dataset so they can collaborate and review - -### Differential resource access restrictions - -As a Dataset Owner I want to grant different levels of access to resources in a dataset so that I can make some resources private and others public (because maybe one resource contains PII) - -### I want to reuse the team/org structure already in LDAP - -As an Org/Team manager I don't want to have to add everyone in my team again in CKAN when i have this already in LDAP so that I save time and avoid risk things go out of sync - - -### Not Permissions (?) - -#### Pre-Release Limits on Datasets - -As a Dataset Owner (?? maybe someone else) I want to have a workflow for reviewing datasets before they go "public" so that they are a) in a good quality state b) are compliant with any regulations (e.g. around PII) - -TODO: Is this really related to permissions?? Seems a broader issue ... diff --git a/site/content/docs/dms/publish.md b/site/content/docs/dms/publish.md deleted file mode 100644 index 60e66397..00000000 --- a/site/content/docs/dms/publish.md +++ /dev/null @@ -1,427 +0,0 @@ -# Publish Data - -## Introduction - -Publish functionality covers the whole area of creating and editing datasets and resources, including data upload. The core job story is something like: - -> When a Data Curator has a data file or dataset they want to add it to their data portal/platform quickly and easily so that it is available there. - -Publication can be divided by its *mode*: - -* **Manual**: publication is done by people via a user interfaces or other tool -* **Programmatic**: publication is done programatically using APIs and is usually part of automated processes -* **Hybrid**: which combines manual and programmatic. An example would be harvesting where setup and configuration may be done in a UI manually with the process then running automatically and programmatically (in addition, some new harvesting flows require manual programmatic setup e.g. writing a harvester in Python for a new source data format). - -**Focus on Manual** we will focus on the manual in this section: programmatic is by nature largely up to the client programmer (assuming the APIs are there) whilst [Harvesting][] has a section of its own. That said, many concepts here are relevant for other cases e.g. material on [profiles][] and [schemas][]. - -**Data uploading**: included in publish is the process of uploading data into the DMS, and specifically into [storage][] and especially [(blob) storage][blob]. -. - -[Harvesting]: /docs/dms/harvesting -[profiles]: /docs/dms/glossary#profile -[schemas]: /docs/dms/glossary#schema -[storage]: /docs/dms/storage -[blob]: /docs/dms/blob-storage - -### Examples - -At its simplest, a publishing process can just involve providing a few metadata fields in a form -- with the data itself stored elsewhere. - -At the other end of the spectrum, we could have a multi-stage and complex process like this: - -* Multiple (simultaneous) resource upload with shared metadata e.g. I'm creating a timeseries dataset with the last 12 months of data and I want each file to share the same column information but to have different titles -* A variety of metadata profiles -* Data validation (prior to ingest) including checking for PII (personally identifiable infromation) -* Complex workflow related to approval e.g. only publish if at least two people have approved -* Embargoing (only make public at time X) - -### Features - -* Create and edit datasets and resources -* File upload as part of resource creation -* Custom metadata for both profile and schemas - -### Job Stories - -When a Data Curator has a data file or dataset they want to add it manually (e.g. via drag and drop etc) to their data portal quickly and easily so that it is avaialble there. - -More specifically: As a Data Curator I want to drop a file in and edit the metadata and have it saved in a really awesome interactive way so that the data is “imported” and of good quality (and i get feedback) - -#### Resources - ->[!tip]A resource is any data item in a dataset e.g. a file. - -When adding a resource to a dataset I want metadata pre-entered for me (e.g. resource name from file name, encoding, ...) to save time and reduce errors - -When adding a resource to a dataset I want to be able to edit the metadata whilst uploading so that I save time - -When uploading a resource's data as part of adding a resource to a dataset I want to see upload progress so that I have a sense of how long this will take - -When adding resources to a dataset I want to be able to add and upload multiple files at once so that I save time and make one big change - -When adding a resource which is tabular (e.g. csv, excel) I want to enter the (table) schema (i.e. the names, description and types of columns) so that my data is more useable, presentable, importable (e.g. to DataStore) and validatable - -When adding a resource which is currently stored in dropbox/gdrive/onedrive I want to pull the bytes directly from there so as to speed up the upload process - -### Domain Model - -The domain model here is that of the [DMS](/docs/dms/dms) and we recommend visiting that page for more information. The key items are: - -* Project -* Dataset -* Resource - -[DMS]: /docs/dms/dms - -### Principles - -* Most ordinary data users don't distinguish resources and datasets in their everyday use. They also prefer a single (denormalized) view onto their data. -* Normalization is not normal for users (it is a convenience, economisation and consistency device) -* And in any case most of us start from files not datasets (even if datasets evolve later). -* Minimize the information the user has to provide to get going. For example, does a user *have* to provide a license to start with? If that is not absolutely required leave this item for later. -* Automate where you can but only where you can guess reliably. If you do guess, give the user ability to modify. Otherwise, magic often turns into mud. For example, if we are guessing file types let the user check and correct this. - -## Flows - -* Publish flows are highly custom: different platforms have different needs -* At the same time there are core components that most people will use (and customize) e.g. uploading a file, adding dataset metadata etc -* The flows shown here are therefore illustrative and inspirational rather than definitive - -### Evolution of a Flow - -Here's a simple illustration of how a publishing flow might evolve: - -```mermaid -graph LR - -a[Add a file] -b[Add metadata] -c[Save] - - -a --> b -b --> c -``` - -```mermaid -graph LR - -a[Add a file] -b[Add metadata] -c[Save] -d[Add table schema] - - -a --> b -b -.-> d -d -.-> c -``` - - -```mermaid -graph LR - -a[Add a file] -b[Add metadata] -c[Save] -d[Add table schema] -e[Check for PII] - -a --> b -b -.-> d -d -.-> e -e --> c -``` - -PII = personally identifiable info - -### The 30,000 foot view - -```mermaid -graph TD - -user[User with data and/or metadata] -publish[Publish process] -platform["Storage (metadata and blobs)"] - -user --> publish --> platform -``` - -### Add Dataset: High Level - -```mermaid -graph TD - -project[Project/Dataset create] -addres["Add resource(s)"] -save[Save / Commit] - -project --> addres -addres --> save -```` - -### Add Dataset: Mid Level - -```mermaid -graph TD - -project[Project/Dataset create] -addres["Add resource(s)"] -addmeta["Add dataset metadata"] -save[Save / Commit] - -project --> addres -addres -.optional.-> addmeta -addmeta -.-> save -addres -.-> save -```` - -The approach above is "file driven" rather than "metadata driven", in the sense that you are start by providing a file rather than providing metadata. - -Here's the metadata driven flow: - -```mermaid -graph TD - -project[Project/Dataset create] -addres["Add resource(s)"] -addmeta[Add dataset metadata] -save[Save / Commit] - -project --> addmeta -addmeta --> addres -addres --> save -addmeta -.-> save -``` - ->[!tip]Comment: The file driven approach is preferable. -We think the "file driven" approach where the flow starts with a user adding and uploading a file (and then adding metadata) is preferable to a metadata driven approach where you start with a dataset and metadata and then add files (as is the default today in CKAN). - -Why do we think a file driven approach is better? a) a file is what the user has immediately to hand b) it is concrete whilst "metadata" is abstract c) common tools for storing files e.g. Dropbox or Google Drive start with providing a file - only later, and optionally, do you rename it, move it etc. - -That said, tools like GitHub or Gitlab require one to create a "project", albeit a minimal one, before being able to push any content. However, GitHub and Gitlab are developer oriented tools that can assume a willingness to tolerate a slightly more cumbersome UX. Furthermore, the default use case is that you have a git repo that you wish to push so the the use case of a non-technical user uploading files is clearly secondary. Finally, in these systems you can create a project just to have an issue tracker or wiki (without having fiile storage). In this case, creating the project first makes sense. - -In a DMS, we are often dealing with non-technical or semi-technical users. Thus, providing a simple, intuitive flow is preferable. That said, one may still have a very lightweight project creation flow so that we have a container for the files (just as in, say, Google Drive you already have a folder to put your files in). - - -### Dataset Metadata editor - -There are lots of ways this can be designed. We always encourage minimalism. - - -* Adding information e.g. license, description, author … -* ? Default the license (and explain what the license means …) - - -### Add a Resource - -From here on, we'll zoom in on the "publish" part of that process. Let's start with the simplest case of adding a single resource in the form of an uploading file: - -```mermaid -graph TD - -addfile[Select a file] -metadata[Add Metadata] -upload[Upload to Storage] -save[Save] - -addfile -.in the background.-> upload -addfile --> metadata -upload -.progress reporting.-> save -metadata --> save -``` - -Notes - -* Alternative to "Select a file" would be to just "Link" to a file that is already online and available - - -### Schema (Data Dictionary) for a Resource - -One part of a publishing flow would be to describe the [schema][] for a resource. Usually, we restrict this to tabular data resources and hence this is a Table Schema. - -[schema]: /docs/dms/glossary#schema - -Usually adding and editing a schema for a resource will be an integrated part of managing the overall metadata for the resource but it can also be a standalone step. The following flow focuses solely on the add schema: - -```mermaid -graph TD - -addfile[Select a file] -infer[Infer the fields, their names and perhaps their types] -edit[Edit the fields and their details e.g. description, types] -save[Save] - -addfile --> infer -infer --> edit -edit --> save -``` - -Notes - -* We recommend using [Frictionless Table Schema][] as format for storing table schema information - -[Frictionless Table Schema]: https://frictionlessdata.io/table-schema/ - -#### Schema editor - -**Fig 1.2: Schema editor wireframe** - - - -* can add title as well as description? Maybe we should have both but i often find them duplicative and why do people want a title …? (For display in charting …) -* Could pivot the display if lots of columns (e.g. have cols down left). This is traditional approach e.g. in CKAN 2 data dictionary - - ![](https://i.imgur.com/nhb5H7Q.png) - -Advanced: - -* Displaying validation errors could/should be live as you change types … (highlight with a hover) -* add semantic/taxonomy option (after format) i.e. ability to set rich type - - -#### Overview Deck - -**Deck**: This deck (Feb 2019) provides an overview of the core flow publishing a single tabular file e.g. CSV and includes a a basic UI mockup illustrating the flow described below. - -