From f026881490f3c13ef7861a5c9cb49a1eb79c334b Mon Sep 17 00:00:00 2001 From: Elizabeth Sall Date: Fri, 14 Jul 2023 14:52:05 -0700 Subject: [PATCH] Add sample data structure, local validation scripts, and documentation Also - update errors/clarity in datapackage.json template --- README.md | 31 + bin/validate-data-package | 68 + bin/validate-data-package-json | 64 + docs/datapackage.md | 59 + docs/samples.md | 9 + main.py | 186 ++- mkdocs.yml | 9 + samples/README.md | 49 + samples/template/README.md | 16 + samples/template/TIDES/datapackage.json | 171 +++ samples/template/TIDES/devices.csv | 1 + samples/template/TIDES/fare_transactions.csv | 1 + samples/template/TIDES/operators.csv | 1 + samples/template/TIDES/passenger_events.csv | 1 + samples/template/TIDES/station_activities.csv | 1 + samples/template/TIDES/stop_visits.csv | 1 + samples/template/TIDES/train_cars.csv | 1 + samples/template/TIDES/trips_performed.csv | 1 + samples/template/TIDES/vehicle_locations.csv | 1 + samples/template/TIDES/vehicle_train_cars.csv | 1 + samples/template/TIDES/vehicles.csv | 1 + .../template/scripts/create_template_files.py | 69 + samples/template/scripts/requirements.txt | 1 + spec/tides-data-package.json | 1296 +++++++++-------- 24 files changed, 1393 insertions(+), 646 deletions(-) create mode 100644 bin/validate-data-package create mode 100644 bin/validate-data-package-json create mode 100644 docs/datapackage.md create mode 100644 docs/samples.md create mode 100644 samples/README.md create mode 100644 samples/template/README.md create mode 100644 samples/template/TIDES/datapackage.json create mode 100644 samples/template/TIDES/devices.csv create mode 100644 samples/template/TIDES/fare_transactions.csv create mode 100644 samples/template/TIDES/operators.csv create mode 100644 samples/template/TIDES/passenger_events.csv create mode 100644 samples/template/TIDES/station_activities.csv create mode 100644 samples/template/TIDES/stop_visits.csv create mode 100644 samples/template/TIDES/train_cars.csv create mode 100644 samples/template/TIDES/trips_performed.csv create mode 100644 samples/template/TIDES/vehicle_locations.csv create mode 100644 samples/template/TIDES/vehicle_train_cars.csv create mode 100644 samples/template/TIDES/vehicles.csv create mode 100644 samples/template/scripts/create_template_files.py create mode 100644 samples/template/scripts/requirements.txt diff --git a/README.md b/README.md index 62de1b4b..a6e372ec 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,37 @@ Human-friendlier documentation is auto-generated and available at: - [Architecture](https://tides-transit.github.io/TIDES/main/architecture) - [Table Schemas](https://tides-transit.github.io/TIDES/main/tables) +## Example Data + +Sample data can be found in the `/samples` directory, with one directory for each example. + +## Validating TIDES data + +TIDES data with a valid [`datapackage.json`](#data-package) can be easily validated using the [frictionless framework](https://framework.frictionlessdata.io/), which can be installed and invoke as follows: + +```bash +pip install frictionless +frictionless validate path/to/your/datapackage.json +``` + +### Data Package + +To validate a package of TIDES data, you must add a frictionless-compliant [`datapackage.json`](https://specs.frictionlessdata.io/data-package/) alongside your data which describes which files should be validated to which schemas. Most of this can be copied from [`/data/template/datapackage.json`](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/data/template/datapackage.json). + +Once this is created, mapping the data files to the schema, simply run: + +```sh +frictionless validate datapackage.json +``` + +### Specific files + +Specific files can be validated by running the frictionless framework against them and their corresponding schemas as follows: + +```sh +frictionless validate vehicles.csv --schema https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/schema.vehicles.json +``` + ## Contributing to TIDES Those who want to help with the development of the TIDES specification should review the guidance in [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/bin/validate-data-package b/bin/validate-data-package new file mode 100644 index 00000000..7f95647e --- /dev/null +++ b/bin/validate-data-package @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# Script: validate_frictionless_package.sh +# Description: Bash script to validate a Frictionless Data Package using the Frictionless CLI. +# Usage: validate_frictionless_package.sh [-v tides_version | -l local_schema_location] [-d dataset_location] +# -v tides_version: Optional. Specify the version of the TIDES specification or 'local' to +# use a local schema. Default is to use the schema specified in the datapackage. +# -l local_schema_location: Optional. Specify the location of the local schema directory. +# Default is '../spec'. Is only used if tides_version = local. +# -d dataset_location: Optional. Specify the location of the TIDES datapackage.json. +# Default is the current directory. + +# Set default values +tides_version="" +local_schema_location="../spec" +dataset_location="." + +# Parse command-line arguments +while getopts ":v:l:d:" opt; do + case $opt in + v) + tides_version=$OPTARG + ;; + l) + local_schema_location=$OPTARG + ;; + d) + dataset_location=$OPTARG + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +# Create a temporary data package if using a different schema reference or a local schema +tmp_datapackage="" +if [ "$tides_version" != "" ] then + tmp_datapackage=$(mktemp) + cp "$dataset_location/datapackage.json" "$tmp_datapackage" +fi + +# Set the schema URL based on the option chosen +schema_url="" +if [ "$tides_version" == "local" ]; then + schema_path_prefix="$local_schema_location" +else + schema_path_prefix="https://raw.githubusercontent.com/TIDES-transit/TIDES/$tides_version/spec" +fi + +# Update the 'schema' property in the temporary copy of the datapackage.json file, if applicable +if [ "$tmp_datapackage" != "" ]; then + schema_file=$(echo "$tmp_datapackage" | sed 's/\//\\\//g') + sed -E -i "s/\"schema\": \"[^\/]+\.schema\.json\"/\"schema\": \"$schema_path_prefix\/\${schema_file##*\/}\"/g" "$tmp_datapackage" + dataset_location="$tmp_datapackage" +fi + +# Validate the data package JSON against the TIDES schema +./validate-data-package-json.sh -v "$tides_version" -d "$dataset_location" -l "$local_schema_location" + +# Validate the Frictionless Data Package using the Frictionless CLI +frictionless validate "$dataset_location" + +# Remove the temporary data package file, if applicable +if [ "$tmp_datapackage" != "" ]; then + rm "$tmp_datapackage" +fi diff --git a/bin/validate-data-package-json b/bin/validate-data-package-json new file mode 100644 index 00000000..504e08ca --- /dev/null +++ b/bin/validate-data-package-json @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# Script to validate a local JSON file against a schema specified in a GitHub repository. +# Usage: validate-data-package-json.sh [-r ref | -l local_schema_location] [-f datapackage_file] +# -r ref: Optional. Specify the ref name of the GitHub repository. Default is 'main'. +# -l local_schema_location: Optional. Specify the location of the local schema directory. +# -f datapackage_file: Optional. Specify the location of the datapackage.json file. Default is 'datapackage.json' in the execution directory. + +# Check if jsonschema-cli is installed +command -v jsonschema-cli >/dev/null 2>&1 || { + echo >&2 "jsonschema-cli is required but not found. You can install it using 'pip install jsonschema-cli'. Aborting." + exit 1 +} + +# Set default values +ref="main" +local_schema_location="" +datapackage_file="datapackage.json" + +# Parse command-line arguments +while getopts ":r:l:f:" opt; do + case $opt in + r) + ref=$OPTARG + ;; + l) + local_schema_location=$OPTARG + ;; + f) + datapackage_file=$OPTARG + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + exit 1 + ;; + esac +done + +echo "Validating data package file in $dataset_location" + +# Set the temporary directory path +temp_dir=$(mktemp -d) + +# Set the schema file path based on the option chosen +schema_file="" +if [ "$local_schema_location" != "" ]; then + schema_file="$local_schema_location/tides-data-package.json" +else + # Download the schema file to the temporary directory + schema_url="https://raw.githubusercontent.com/TIDES-transit/TIDES/$ref/spec/tides-data-package.json" + schema_file="$temp_dir/data-package.json" + + if curl -s --head "$schema_url/tides-data-package.json" >/dev/null; then + echo "Schema file not found on GitHub for the specified TIDES version: $tides_version" + exit 1 + fi +curl -o "$schema_file" "$schema_url" +fi + +# Validate datapackage against the downloaded schema +jsonschema-cli validate "$schema_file" "$datapackage_file" + +# Clean up the temporary directory +rm -rf "$temp_dir" diff --git a/docs/datapackage.md b/docs/datapackage.md new file mode 100644 index 00000000..3f04945e --- /dev/null +++ b/docs/datapackage.md @@ -0,0 +1,59 @@ +# Data Package + +TIDES data must include a `datapackage.json` in the format specified by the [`tides-data-package` json schema](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/tides-data-package.json), which is an extension of the [frictionless data package](https://specs.frictionlessdata.io/data-package/) schema. + +You may create your own `datapackage.json` based on the documentaiton or start with the provided [template](#template), but don't forget to [validate](#validation) it to make sure it is in the correct format! + +## Data Package Format + +{{ frictionless_data_package('spec/tides-data-package.json') }} + +## Tabular Data Resource + +Required and recommended fields for each `tabluar-data-resource` are as follows: + +{{ frictionless_data_package('spec/tides-data-package.json',sub_schema="resources") }} + +## Template + +The canonical `datapackage.json` template is available at [`/data/template/TIDES/datapackage.json`](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/samples/template/TIDES/datapackage.json). + +!!! warning + This version of `tides-data-package` template is dependent on the version of the documentation you are viewing and only represents the canonical `tides-data-package` template if you are viewing the `main` documentation version. + +{{ include_file('samples/template/TIDES/datapackage.json',code_type='json') }} + +## Validation + +There are lots of options for validating your `datapackage.json` file including: + +- [Command Line Interface (CLI) Script](#cli) +- [Various online websites](#point-and-drool) + +### CLI + +You can easily validate your data package file with the script provided in [`/bin/validate-data-package-json`](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/bin/validate-data-package-json) + +??? tip "installation requirements" + + Make sure you have jsonschema-cli installed. You can install it specifically or with all of the other suggested tools using one of the commands below: + + ```sh + pip install jsonschema-cli + pip install -r requirements.txt + ``` + +```sh title="usage" +validate-data-package-json -f my-datapackage.json +``` + +{{ include_file('bin/validate-data-package-json',code_type='sh') }} + +### Point-and-Drool + +Because a `tides-data-package` is just a json-schema, you can use the myriad of different json-schema validator out there on the web. Use the [canonical `tides-data-package`](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/tides-data-package.json) or copy and paste the version from below. + +!!! warning + This version of `tides-data-package` is dependent on the version of the documentation you are viewing and only represents the canonical `tides-data-package` if you are viewing the `main` documentation version. + +{{ include_file('spec/tides-data-package.json',code_type='json') }} diff --git a/docs/samples.md b/docs/samples.md new file mode 100644 index 00000000..7b4125ca --- /dev/null +++ b/docs/samples.md @@ -0,0 +1,9 @@ +# Sample Data + +Sample data can be found in the `/samples` directory, with one directory for each data sample. + +{{ include_file('samples/README.md')}} + +## Data List + +{{ list_samples('samples') }} diff --git a/main.py b/main.py index fada5b1f..38d9037b 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,15 @@ import glob import json +import logging import os import pathlib import re -from typing import Union +from typing import Union, Literal import pandas as pd +log = logging.getLogger("mkdocs") + FIND_REPLACE = { # original relative to /docs : redirect target "": "[Contributing Section](development/#CONTRIBUTING)", "(CODE_OF_CONDUCT.md)": "(development/#CODE_OF_CONDUCT)", @@ -26,9 +29,39 @@ def define_env(env): - macro: a decorator function, to declare a macro. """ + @env.macro + def list_samples(sample_dir: str) -> str: + """Outputs a simple list of the directories in a folder in markdown. + Args: + sample_dir (str):directory to search in + Returns: + str: markdown-formatted list + """ + EXCLUDE = ["template"] + fields = ["Sample", "Agency", "Resources", "Vendors"] + + sample_dir = os.path.join(env.project_dir, sample_dir) + samples = [ + d + for d in os.listdir(sample_dir) + if os.path.isdir(os.path.join(sample_dir, d)) and d not in EXCLUDE + ] + + md_table = ( + "| *" + "** | **".join(fields) + "* |\n| " + " ----- |" * len(fields) + "\n" + ) + + for s in samples: + md_table += datapackage_to_row(os.path.join(sample_dir, s, "TIDES"), fields) + return md_table + @env.macro def include_file( - filename: str, downshift_h1=True, start_line: int = 0, end_line: int = None + filename: str, + downshift_h1=True, + start_line: int = 0, + end_line: int = None, + code_type: str = None, ): """ Include a file, optionally indicating start_line and end_line. @@ -42,6 +75,8 @@ def include_file( start_line (Optional): if included, will start including the file from this line (indexed to 0) end_line (Optional): if included, will stop including at this line (indexed to 0) + code_type: if not None, will encapsulate the resulting file in + ```..file contents...``` """ full_filename = os.path.join(env.project_dir, filename) with open(full_filename, "r") as f: @@ -71,26 +106,118 @@ def include_file( _replace = _replace.replace(_filenamebase, "") content = content.replace(_find, _replace) + + if code_type is not None: + content = f"\n```{code_type} title='{filename}'\n{content}\n```" return content + @env.macro + def frictionless_data_package( + data_package_path: str = "**/data-package.json", + sub_schema: str = None, + include: Literal["required", "recommended", "all"] = "recommended", + ) -> str: + """Describes top-level fields of a data package as a markdown table. + + Note: Right now if it finds more than one, it will take the first one. + + Args: + data_package_path (str, optional): location or glob pattern of data package. If glob, + will use the first found match. + sub_schema: if provided, will look for that portion of the schema and use that as + the "top level" + include: specifies the fields to include. Must be one of: + - "required": will only document required fields + - "recommended": will document required and recommended fields + - "all": will document all fields + Defaults to "recommended". + + Returns: a markdown table string + """ + INCLUDE = ["name", "description", "type", "requirement"] + + dp_filename = glob.glob(data_package_path, recursive=True)[0] + + log.info( + f"Documenting spec_file: {dp_filename}.{sub_schema} including {include}" + ) + + with open(dp_filename, "r") as dp_file: + dp = json.loads(dp_file.read()) + + if sub_schema is not None: + if sub_schema in dp["properties"]: + dp = dp["properties"][sub_schema] + elif sub_schema in dp["$defs"]: + dp = dp["$defs"][sub_schema] + else: + return "Cannot find sub-schema {sub_schema} in data package file {dp_filename}" + if dp["type"] == "array": + dp = dp["items"] + + _field_names = [] + + if include in ["required", "recommended"]: + _field_names += dp.get("required", []) + if include == "recommended": + _field_names += dp.get("recommended", []) + if include == "all": + _field_names = list(dp["properties"].keys()) + + if not _field_names: + return "No fields found to document with parameters." + + _fields = [] + for _f in _field_names: + log.debug(f"Documenting: {_f}") + _row_entry = {k: v for k, v in dp["properties"][_f].items() if k in INCLUDE} + if _f in dp.get("required", []): + _row_entry["requirement"] = "required" + elif _f in dp.get("recommended", []): + _row_entry["requirement"] = "recommended" + else: + _row_entry["requirement"] = " - " + + _row_entry["name"] = f"`{_f}`" + if dp["properties"][_f].get("enum"): + _row_entry["description"] += "
**Must** be one of:
  • `" + _row_entry["description"] += "`
  • `".join( + dp["properties"][_f]["enum"] + ) + _row_entry["description"] += "`
" + elif dp["properties"][_f].get("const"): + _row_entry[ + "description" + ] += f"
**Must** be: `{dp['properties'][_f]['const']}`" + elif dp["properties"][_f].get("examples"): + _ex = dp["properties"][_f]["examples"][0].replace("\n", "
") + _row_entry["description"] += f"
**Example**:
`{_ex}`" + + _fields.append(_row_entry) + + dp_df = pd.DataFrame(_fields, columns=INCLUDE) + dp_md = dp_df.to_markdown(index=False) + + return dp_md + @env.macro def frictionless_spec( spec_path: str = "**/*.spec.json", ) -> str: """Translate the frictionless .spec file to a markdown table. - Note: Right now if it finds more than one, it will take the first one. + Note: Right now if it finds more than one, it will take the first one. If glob, + will use the first found match. Args: - spec_path (str, optional): base path of repo. Defaults to two directories - up from this file. + spec_path (str, optional): location or glob pattern of spec Returns: a markdown table string """ spec_file = glob.glob(spec_path, recursive=True)[0] - print("Documenting spec_file: ", spec_file) + log.info(f"Documenting spec_file: {spec_file}") # Generate a table for overall file requirements spec_df = read_config(spec_file) @@ -117,7 +244,7 @@ def frictionless_schemas( # Create markdown with a table for each schema file schema_files = glob.glob(schema_path, recursive=True) - print("Documenting schemas from: {}".format(schema_files)) + log.info("Documenting schemas from: {schema_files}") file_schema_markdown = [ _document_frictionless_schema(_s) for _s in schema_files @@ -136,6 +263,49 @@ def frictionless_schemas( TABLE_TYPES = ["Event", "Summary", "Supporting"] +def datapackage_to_row(dir: str, fields: list) -> str: + """Reads datapackage and translates key metadata to a row in a markdown table. + + Right now, only works for fields: "Sample", "Agency", "Resources", "Vendors" + + Args: + dir (str): fully qualified directory for sample data + fields (list): list of fields to return + + Returns: + str: row in a markdown table with name, agency, resources, vendors + """ + dp_filename = os.path.join(dir, "datapackage.json") + if not os.path.isfile(dp_filename): + raise ValueError(f"Can't find datapackage file:\n {dp_filename}.") + with open(dp_filename, "r") as dp_file: + dp = json.loads(dp_file.read()) + + _vendors = [ + s.get("vendor", None) for r in dp["resources"] for s in r.get("sources", []) + ] + _vendors = list(set(_vendors) - set([None])) + + _md_cells = { + "Sample": _to_sample_readme_link(dp["name"], dir), + "Agency": dp["agency"], + "Resources": ( + "
  • `" + + "`
  • `".join([r["name"] for r in dp["resources"]]) + + "`
" + ), + "Vendors": "
  • `" + "`
  • `".join(_vendors) + "`
", + } + + md_row = "| " + " | ".join([_md_cells[f] for f in fields]) + " |\n" + + return md_row + + +def _to_sample_readme_link(sample_name, folder_dir): + return f"[{sample_name.capitalize()}]({folder_dir}/README.md)" + + def _document_frictionless_schema(schema_filename: str) -> dict: """Documents a single frictionless schema. @@ -149,7 +319,7 @@ def _document_frictionless_schema(schema_filename: str) -> dict: "schema_md": markdown table documenting fields """ - print(f"Documenting schema from file: {schema_filename}") + log.info(f"Documenting schema from file: {schema_filename}") spec_name = schema_filename.split("/")[-1].split(".")[0] spec_title = " ".join([x.capitalize() for x in spec_name.split("_")]) schema = read_schema(schema_filename) diff --git a/mkdocs.yml b/mkdocs.yml index aae3d55c..a818df60 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ edit_uri: edit/main/docs theme: name: material features: + - content.code.copy - navigation.tabs - navigation.expand - toc.integrate @@ -34,8 +35,14 @@ plugins: theme: | ^(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light' securityLevel: 'loose' + - redirects: + redirect_maps: #original relative to /docs : redirect target + '../CODE_OF_CONDUCT.md': 'CODE_OF_CONDUCT.md' + '../CONTRIBUTING.md': 'CONTRIBUTING.md' + '../contributors.md': 'contributors.md' - search + watch: - CONTRIBUTING.md - CODE_OF_CONDUCT.md @@ -74,4 +81,6 @@ nav: - Home: index.md - Architecture: architecture.md - Table Schemas: tables.md + - Data Package: datapackage.md + - Sample Data: samples.md - Development: development.md diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 00000000..e529f357 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,49 @@ +# Samples Directory Organization + +Each TIDES Data Package example should follow the following directory structure, consistent with the structure of the [Frictionless Data Package specification](https://specs.frictionlessdata.io/data-package/), including: + +```sh +unique-example-name + \TIDES # Required. Data to be validated against the TIDES specification + \datapackage.json # Required. [`tides-data-package`](#tides-data-package) metadata + \raw # Optional. Data which the agency uses to create TIDES data + \scripts # Optional. Scripts used to transform raw --> TIDES +``` + +## Adding Examples + +We encourage the addition of examples, but please follow the following guidelines: + +1. *No large files* This isn't the place to store your data, rather to document some minimal examples. The recommended size is 100-1000 records per file, more if absolutely required to reproduce an issue with the spec. All individual files should be well under 50 MB. +2. *Include Metadata* as specified in [tides-data-package](#data-package). +3. *Include a README.md* in the base folder of your example with an overview so that it can be included in the documentation. + +## Data Package + +TIDES sample data must include a `datapackage.json` in the format specified by the [`tides-data-package` json schema](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/tides-data-package.json) (an extension of the [frictionless data package](https://specs.frictionlessdata.io/data-package/)). + +See: + +- [Full documentation on the `tides-data-package`]({{ site_url }}/datapackage) +- [Example `tides-data-package` file](https://raw.githubusercontent.com/TIDES-transit/TIDES/main/samples/template) + +## Data validation + +Data with a valid [`datapackage.json`](#data-package) can be easily validated using the [frictionless framework](https://framework.frictionlessdata.io/), which can be installed and invoked as follows: + +```bash +pip install frictionless +frictionless validate --schema-sync path/to/your/datapackage.json +``` + +### Specific files + +Specific files can be validated by running the frictionless framework against them and their corresponding schemas as follows: + +```sh +frictionless validate vehicles.csv --schema https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/vehicles.schema.json +``` + +### Continuous Data Validation + +Sample data in the `\TIDES` subdirectories of each sample is validated upon a push action to the main repository. diff --git a/samples/template/README.md b/samples/template/README.md new file mode 100644 index 00000000..1cdaf7f0 --- /dev/null +++ b/samples/template/README.md @@ -0,0 +1,16 @@ +# Example TIDES Data Package + +Template directory for example scaffolding and helper scripts. + +## Scripts for Generating Template Data + +`scripts\create_template.py` has some template code which can help with the following + +- `write_schema_examples()`: will generate blank csvs according to the TIDES schema +- `write_datapackage()` will generate a datapackage.json based on the TIDES schemas and a set of defaults specified in th script. + +To run both (note this replaces the existing files in the directory) + +```bash +python samples/template/scripts/create_template.py +``` diff --git a/samples/template/TIDES/datapackage.json b/samples/template/TIDES/datapackage.json new file mode 100644 index 00000000..0609c7de --- /dev/null +++ b/samples/template/TIDES/datapackage.json @@ -0,0 +1,171 @@ +{ + "name": "template", + "title": "Template TIDES Data Package Example", + "agency": "Transit Agency Name", + "ntd_id": "1234-56", + "profile": "tabular-data-package", + "licenses": [ + { + "name": "Apache-2.0" + } + ], + "contributors": [ + { + "title": "My Name", + "email": "me@myself.com", + "github": "myhandle" + } + ], + "maintainers": [ + { + "title": "Another Name", + "email": "another@myself.com", + "github": "myhandle" + } + ], + "resources": [ + { + "name": "devices", + "path": "devices.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/devices.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "vehicle_locations", + "path": "vehicle_locations.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/vehicle_locations.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "fare_transactions", + "path": "fare_transactions.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/fare_transactions.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "train_cars", + "path": "train_cars.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/train_cars.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "operators", + "path": "operators.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/operators.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "stop_visits", + "path": "stop_visits.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/stop_visits.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "vehicle_train_cars", + "path": "vehicle_train_cars.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/vehicle_train_cars.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "vehicles", + "path": "vehicles.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/vehicles.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "trips_performed", + "path": "trips_performed.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/trips_performed.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "station_activities", + "path": "station_activities.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/station_activities.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + }, + { + "name": "passenger_events", + "path": "passenger_events.csv", + "schema": "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/passenger_events.schema.json", + "sources": [ + { + "title": "Where did data come from?", + "component": "Type of technology component, i.e. `AVL`", + "product": "Product used.", + "vendor": "Vendor selling product." + } + ] + } + ] +} diff --git a/samples/template/TIDES/devices.csv b/samples/template/TIDES/devices.csv new file mode 100644 index 00000000..6540cce5 --- /dev/null +++ b/samples/template/TIDES/devices.csv @@ -0,0 +1 @@ +device_id,stop_id,vehicle_id,train_car_id,device_type,device_vendor,device_model,location diff --git a/samples/template/TIDES/fare_transactions.csv b/samples/template/TIDES/fare_transactions.csv new file mode 100644 index 00000000..3f65e125 --- /dev/null +++ b/samples/template/TIDES/fare_transactions.csv @@ -0,0 +1 @@ +transaction_id,date,timestamp,amount,currency_type,fare_action,trip_id_performed,stop_sequence,vehicle_id,device_id,fare_id,stop_id,group_size,media_type,rider_category,fare_product,fare_period,fare_capped,fare_media_id,fare_media_id_purchased,balance diff --git a/samples/template/TIDES/operators.csv b/samples/template/TIDES/operators.csv new file mode 100644 index 00000000..713abf28 --- /dev/null +++ b/samples/template/TIDES/operators.csv @@ -0,0 +1 @@ +operator_id diff --git a/samples/template/TIDES/passenger_events.csv b/samples/template/TIDES/passenger_events.csv new file mode 100644 index 00000000..34932008 --- /dev/null +++ b/samples/template/TIDES/passenger_events.csv @@ -0,0 +1 @@ +passenger_event_id,date,timestamp,trip_id_performed,stop_sequence,event_type,vehicle_id,device_id,train_car_id,stop_id diff --git a/samples/template/TIDES/station_activities.csv b/samples/template/TIDES/station_activities.csv new file mode 100644 index 00000000..cbbc9d94 --- /dev/null +++ b/samples/template/TIDES/station_activities.csv @@ -0,0 +1 @@ +date,stop_id,time_period_start,time_period_end,time_period_category,total_entries,total_exits,number_of_transactions,transaction_revenue_cash,transaction_revenue_smartcard,transaction_revenue_magcard,transaction_revenue_bankcard,transaction_revenue_nfc,transaction_revenue_optical,transaction_revenue_operator,transaction_revenue_other,transaction_count_cash,transaction_count_smartcard,transaction_count_magcard,transaction_count_bankcard,transaction_count_nfc,transaction_count_optical,transaction_count_operator,transaction_count_other,bike_entries,bike_exits,ramp_entries,ramp_exits diff --git a/samples/template/TIDES/stop_visits.csv b/samples/template/TIDES/stop_visits.csv new file mode 100644 index 00000000..2801b88c --- /dev/null +++ b/samples/template/TIDES/stop_visits.csv @@ -0,0 +1 @@ +date,trip_id_performed,stop_sequence,vehicle_id,dwell,stop_id,checkpoint,schedule_arrival_time,schedule_departure_time,actual_arrival_time,actual_departure_time,distance,boarding_1,alighting_1,boarding_2,alighting_2,load,door_open,door_close,door_status,ramp_deployed_time,ramp_failure,kneel_deployed_time,lift_deployed_time,bike_rack_deployed,bike_load,revenue,number_of_transactions,transaction_revenue_cash,transaction_revenue_smartcard,transaction_revenue_magcard,transaction_revenue_bankcard,transaction_revenue_nfc,transaction_revenue_optical,transaction_revenue_operator,transaction_revenue_other,transaction_count_cash,transaction_count_smartcard,transaction_count_magcard,transaction_count_bankcard,transaction_count_nfc,transaction_count_optical,transaction_count_operator,transaction_count_other,schedule_relationship diff --git a/samples/template/TIDES/train_cars.csv b/samples/template/TIDES/train_cars.csv new file mode 100644 index 00000000..3b0e604b --- /dev/null +++ b/samples/template/TIDES/train_cars.csv @@ -0,0 +1 @@ +train_car_id,model_name,facility_name,capacity_seated,wheelchair_capacity,bike_capacity,bike_rack,capacity_standing,train_car_type diff --git a/samples/template/TIDES/trips_performed.csv b/samples/template/TIDES/trips_performed.csv new file mode 100644 index 00000000..ba27e909 --- /dev/null +++ b/samples/template/TIDES/trips_performed.csv @@ -0,0 +1 @@ +date,trip_id_performed,vehicle_id,trip_id_scheduled,route_id,route_type,shape_id,direction_id,operator_id,block_id,trip_start_stop_id,trip_end_stop_id,schedule_trip_start,schedule_trip_end,actual_trip_start,actual_trip_end,in_service,schedule_relationship diff --git a/samples/template/TIDES/vehicle_locations.csv b/samples/template/TIDES/vehicle_locations.csv new file mode 100644 index 00000000..5dff84a7 --- /dev/null +++ b/samples/template/TIDES/vehicle_locations.csv @@ -0,0 +1 @@ +location_ping_id,date,timestamp,trip_id_performed,stop_sequence,vehicle_id,device_id,stop_id,current_status,latitude,longitude,gps_quality,heading,speed,odometer,schedule_deviation,headway_deviation,in_service,schedule_relationship diff --git a/samples/template/TIDES/vehicle_train_cars.csv b/samples/template/TIDES/vehicle_train_cars.csv new file mode 100644 index 00000000..9044359c --- /dev/null +++ b/samples/template/TIDES/vehicle_train_cars.csv @@ -0,0 +1 @@ +vehicle_id,train_car_id,order,operator_id diff --git a/samples/template/TIDES/vehicles.csv b/samples/template/TIDES/vehicles.csv new file mode 100644 index 00000000..67ef59e8 --- /dev/null +++ b/samples/template/TIDES/vehicles.csv @@ -0,0 +1 @@ +vehicle_id,vehicle_start,vehicle_end,model_name,facility_name,capacity_seated,wheelchair_capacity,capacity_bike,bike_rack,capacity_standing diff --git a/samples/template/scripts/create_template_files.py b/samples/template/scripts/create_template_files.py new file mode 100644 index 00000000..a9122f36 --- /dev/null +++ b/samples/template/scripts/create_template_files.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +""" +Generates a set of template files using structure and defaults of **schema.json + +--output-path Saves it in the specified output directory (/path/to/output). +If the argument is not provided, it will default to the same directory as the script. + +--schema can specify using a specific schema instead of the default of all +""" + +import argparse +import glob +import logging +import os +import pathlib + +from frictionless import Schema + +USAGE = """ +python samples/template/scripts/create_template_files.py --output-dir /path/to/output +""" + + +SAMPLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_REPO_DIR = os.path.dirname(os.path.dirname(SAMPLE_DIR)) +SCHEMAS = glob.glob( + os.path.join(os.path.join(BASE_REPO_DIR, "spec"), "**/*.schema.json"), + recursive=True, +) +DEFAULT_OUT_PATH = "../TIDES" + + +def write_template_for_schema( + schema_filename: str, + out_dir: str, +) -> None: + """Creates blank csvs which comply with a schema. + + Args: + schema_filename (str): Filename with the Frictionless data schema + out_dir (str): Where the csv will be written + """ + _schema = Schema(schema_filename) + fields = [s.name for s in _schema.fields] + _schema_name = pathlib.Path(schema_filename).stem.split(".")[0] + out_filename = os.path.join(out_dir, _schema_name + ".csv") + with open(out_filename, "w") as outfile: + logging.info(f"Writing: {out_filename}") + outfile.write(",".join(fields)) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + parser = argparse.ArgumentParser(description="Generate a template csvs.") + parser.add_argument( + "--schema", + default=SCHEMAS, + help="List of paths to frictionless schemas to generate. Default: all in /spec dir.", + ) + parser.add_argument( + "--output-path", + default=DEFAULT_OUT_PATH, + help="Directory to save the generated files. Default: same directory as script.", + ) + args = parser.parse_args() + + for s in args.schema: + write_template_for_schema(s, args.output_path) diff --git a/samples/template/scripts/requirements.txt b/samples/template/scripts/requirements.txt new file mode 100644 index 00000000..140b95f1 --- /dev/null +++ b/samples/template/scripts/requirements.txt @@ -0,0 +1 @@ +frictionless diff --git a/spec/tides-data-package.json b/spec/tides-data-package.json index 8d055dd5..40078576 100644 --- a/spec/tides-data-package.json +++ b/spec/tides-data-package.json @@ -1,688 +1,708 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "TIDES Data Package", - "description": "TIDES Data Package is a simple specification for data access and delivery of tabular TIDES transit data.", - "type": "object", - "required": [ - "profile", - "resources", - "title" - ], - "properties": { - "profile": { - "enum": [ - "tides-data-package" - ], - "propertyOrder": 10, - "title": "Profile", - "description": "The profile of this descriptor.", - "context": "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.", - "type": "string", - "examples": [ - "{\n \"profile\": \"tides-data-package\"\n}\n", - "{\n \"profile\": \"http://example.com/my-profiles-json-schema.json\"\n}\n" - ] - }, - "title": { - "propertyOrder": 20, - "title": "Title", - "description": "A human-readable title.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ] - }, - "name": { - "propertyOrder": 30, - "title": "Name", - "description": "Short sluggable (https://en.wikipedia.org/wiki/Clean_URL#Slug) identifier string.", - "type": "string", - "pattern": "^([-a-z0-9._/])+$", - "context": "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.", - "examples": [ - "{\n \"name\": \"tides-data-package\"\n}\n" - ] - }, - "description": { - "propertyOrder": 40, - "format": "textarea", - "title": "Description", - "description": "Short description of TIDES data package.", - "type": "string", - "examples": [ - "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" - ] - }, - "agency": { - "propertyOrder": 50, - "format": "textarea", - "title": "Agency", - "description": "Transit agency name.", - "type": "string", - "examples": [ - "{\n \"agency\": \"Coos County Area Transit\"\n}\n" - ] - }, - "ntd_id": { - "propertyOrder": 60, - "title": "NTD ID", - "description": "ID for the National Transit Database.", - "type": "string", - "pattern": "^([-a-z0-9._/])+$", - "context": "Background on NTD available at https://www.transit.dot.gov/ntd", - "examples": [ - "{\n \"ntd_id\": \"0R02-00307\"\n}\n" - ] - }, - "contributors": { - "propertyOrder": 70, - "title": "Contributors", - "description": "Array of data contributors `[{\"title\": \"My Name\", \"github\": \"my_handl\", \"email\": \"me@myself.com\"}]`", - "type": "array", - "minItems": 1, - "items": { - "title": "Contributor", - "description": "A contributor to this descriptor.", - "properties": { - "name": { - "title": "Name", - "description": "A human-readable name.", - "type": "string", - "examples": [ - "{\n \"title\": \"Frank Julian Sprague\"\n}\n" - ] - }, - "email": { - "title": "Email", - "description": "An email address.", - "type": "string", - "format": "email", - "examples": [ - "{\n \"email\": \"example@example.com\"\n}\n" - ] - }, - "organization": { - "title": "Organization", - "description": "An organizational affiliation for this contributor.", - "type": "string" - }, - "role": { - "title": "Role", - "description": "The contributor's role in the project", - "type": "string", - "default": "contributor" - } + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TIDES Data Package", + "description": "TIDES Data Package is a simple specification for data access and delivery of tabular TIDES transit data.", + "type": "object", + "required": [ + "title", + "profile", + "resources" + + ], + "recommended":[ + "name", + "description", + "agency", + "ntd_id", + "licenses", + "contributors", + "maintainers" + ], + "properties": { + "profile": { + "const": "tides-data-package", + "propertyOrder": 10, + "title": "Profile", + "description": "The json-schema profile used to validate this datapackage descriptor.", + "context": "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.", + "type": "string" + }, + "title": { + "propertyOrder": 20, + "title": "Title", + "description": "A human-readable title.", + "type": "string", + "examples": [ + "Make Believe Trolley 1/1/2001-2/1/2001" + ] + }, + "name": { + "propertyOrder": 30, + "title": "Name", + "description": "Short, unique [sluggable](https://en.wikipedia.org/wiki/Clean_URL#Slug) identifier string.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "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.", + "examples": [ + "trolley_2001_1" + ] + }, + "description": { + "propertyOrder": 40, + "format": "textarea", + "title": "Description", + "description": "Short description of TIDES data package.", + "type": "string", + "examples": [ + "Raw and processed data from the Neighborhood of Make Believe Trolley recorded 1/1/2001-2/1/2001" + ] + }, + "agency": { + "propertyOrder": 50, + "format": "textarea", + "title": "Agency", + "description": "Transit agency name.", + "type": "string", + "examples": [ + "Make Believe Neighborhood Trolley" + ] + }, + "ntd_id": { + "propertyOrder": 60, + "title": "NTD ID", + "description": "ID for the National Transit Database.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "Background on NTD available at https://www.transit.dot.gov/ntd", + "examples": [ + "0R02-00307" + ] + }, + "contributors": { + "propertyOrder": 70, + "title": "Contributors", + "description": "Array of data contributors.", + "type": "array", + "minItems": 1, + "items": { + "title": "Contributor", + "description": "A contributor to this descriptor.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "A human-readable name.", + "type": "string", + "examples": [ + "Daniel Tiger" + ] + }, + "email": { + "title": "Email", + "description": "An email address.", + "type": "string", + "format": "email", + "examples": [ + "daniel@makebelievetrolley.gov" + ] }, - "required": [ - "title" - ], - "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." + "organization": { + "title": "Organization", + "description": "An organizational affiliation for this contributor.", + "type": "string", + "example": "Make Believe Neighborhood Trolley" + }, + "role": { + "title": "Role", + "description": "The contributor's role in the project", + "type": "string", + "default": "contributor" + } }, - "examples": [ - "{\n \"contributors\": [\n {\n \"name\": \"Joe Bloggs\"\n }\n ]\n}\n", - "{\n \"contributors\": [\n {\n \"name\": \"Miriam Coates\",\n \"email\": \"joe@example.com\",\n \"role\": \"author\"\n }\n ]\n}\n" - ] + "required": [ + "title" + ], + "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." }, - "maintainers": { - "propertyOrder": 80, - "title": "Maintainers", - "description": "Array of data maintainers `[{\"title\": \"My Name\", \"github\": \"my_handl\", \"email\": \"me@myself.com\"}]`", - "type": "array", - "minItems": 1, - "items": { - "title": "Maintainer", - "description": "A maintainer of this descriptor.", - "properties": { - "name": { - "title": "Name", - "description": "A human-readable name.", - "type": "string", - "examples": [ - "{\n \"name\": \"George Francis Train\"\n}\n" - ] - }, - "email": { - "title": "Email", - "description": "An email address.", - "type": "string", - "format": "email", - "examples": [ - "{\n \"email\": \"example@example.com\"\n}\n" - ] - }, - "organization": { - "title": "Organization", - "description": "An organizational affiliation for this maintainer.", - "type": "string" - }, - "role": { - "title": "Role", - "description": "The maintainer's role in the project", - "type": "string", - "default": "maintainer" - } + "examples": [ + "{'name':'Daniel Tiger','email': 'daniel@makebelievetrolley.gov','organization':'Make Believe Neighborhood Trolley','role':'contributor'}" + ] + }, + "maintainers": { + "propertyOrder": 80, + "title": "Maintainers", + "description": "Array of data maintainers", + "type": "array", + "minItems": 1, + "items": { + "title": "Maintainer", + "description": "A maintainer of this descriptor.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "A human-readable name.", + "type": "string", + "examples": [ + "Henrietta Pussycat" + ] + }, + "email": { + "title": "Email", + "description": "An email address.", + "type": "string", + "format": "email", + "examples": [ + "henrietta@makebelievetrolley.gov" + ] + }, + "organization": { + "title": "Organization", + "description": "An organizational affiliation for this maintainer.", + "type": "string" }, - "required": [ - "title" - ], - "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." + "role": { + "title": "Role", + "description": "The maintainer's role in the project", + "type": "string", + "default": "maintainer" + } }, - "examples": [ - "{\n \"maintainers\": [\n {\n \"name\": \"Joe Bloggs\"\n }\n ]\n}\n", - "{\n \"maintainers\": [\n {\n \"name\": \"Miriam Coates\",\n \"email\": \"joe@example.com\",\n \"role\": \"author\"\n }\n ]\n}\n" - ] + "required": [ + "title" + ], + "context": "Use of this property does not imply that the person was the original creator of, or a contributor to, the data in the descriptor, but refers to the composition of the descriptor itself." }, - "licenses": { - "propertyOrder": 90, - "title": "Licenses", - "description": "The license(s) under which this package is published.", - "type": "array", - "minItems": 1, - "items": { - "title": "License", - "description": "A license for this descriptor.", - "type": "object", - "anyOf": [ - { - "required": [ - "name" - ] - }, - { - "required": [ - "path" - ] - } - ], - "properties": { - "name": { - "title": "Open Definition license identifier", - "description": "MUST be an Open Definition license identifier, see http://licenses.opendefinition.org/", - "type": "string", - "pattern": "^([-a-zA-Z0-9._])+$" - }, - "path": { - "title": "Path", - "description": "A fully qualified public URL, or a Unix-style relative file path.", - "type": "string", - "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", - "examples": [ - "{\n \"path\": \"/license.md\"\n}\n", - "{\n \"path\": \"https://opensource.org/license/gpl-2-0/\"\n}\n" - ], - "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." - }, - "title": { - "title": "Title", - "description": "A human-readable title.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ] - } + "examples": [ + "{'name':'Henrietta Pussycat','email': 'henrietta@makebelievetrolley.gov','organization':'Make Believe Neighborhood Trolley','role':'maintainer'}" + + ] + }, + "licenses": { + "propertyOrder": 90, + "title": "Licenses", + "description": "The license(s) under which this package is published.", + "type": "array", + "minItems": 1, + "items": { + "title": "License", + "description": "An array of license objects which apply to this data package. Must include at least a `name` or `path`", + "type": "object", + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] } - }, - "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", - "examples": [ - "{\n \"licenses\": [\n {\n \"name\": \"odc-pddl-1.0\",\n \"path\": \"http://opendatacommons.org/licenses/pddl/\",\n \"title\": \"Open Data Commons Public Domain Dedication and License v1.0\"\n }\n ]\n}\n" - ] + ], + "properties": { + "name": { + "title": "Open Definition license identifier", + "description": "MUST be an Open Definition license identifier, see http://licenses.opendefinition.org/", + "type": "string", + "pattern": "^([-a-zA-Z0-9._])+$", + "examples": ["odc-by","CC-BY-4.0"] + }, + "path": { + "title": "Path", + "description": "A fully qualified public URL, or a Unix-style relative file path.", + "type": "string", + "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", + "examples": [ + "https://opendefinition.org/licenses/odc-by/", + "license.md" + ], + "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." + }, + "title": { + "title": "Title", + "description": "A human-readable title of the license.", + "type": "string", + "examples": [ + "Creative Commons Attribution 4.0" + ] + } + } }, - "resources": { - "propertyOrder": 100, - "title": "Tabular Data Resources", - "description": "Array of data files included in your package, formated as a tabular-data-resource", - "type": "array", - "minItems": 1, - "items": { - "title": "Tabular Data Resource", - "description": "A Tabular Data Resource.", - "type": "object", - "required": [ + "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", + "examples": [ + "{'name':'CC-BY-4.0','path':'https://opendefinition.org/licenses/cc-by/','name':'Creative Commons Attribution 4.0'}" + ] + }, + "resources": { + "propertyOrder": 100, + "title": "Tabular Data Resources", + "description": "Array describing the data files included in your package, formated as a [`tabular-data-resource`](#tabular-data-resource)", + "type": "array", + "minItems": 1, + "items": { + "title": "Tabular Data Resource", + "description": "A Tabular Data Resource.", + "type": "object", + "required": [ + "name", + "path", + "schema", + "profile" + ], + "recommended": [ + "title", + "description", + "sources", + "licenses" + ], + "properties": { + "profile": { + "const": "tabular-data-resource", + "propertyOrder": 10, + "title": "Profile", + "description": "Must be `tabular-data-resource`", + "type": "string", + "examples": [ + "tabular-data-resource" + ] + }, + "name": { + "propertyOrder": 20, + "title": "Name", + "description": "Short, unique [sluggable](https://en.wikipedia.org/wiki/Clean_URL#Slug) identifier string for a data table.", + "type": "string", + "pattern": "^([-a-z0-9._/])+$", + "context": "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.", + "examples": [ + "vehicle_locations_2001_01" + ] + }, + "path": { + "propertyOrder": 30, + "title": "Path", + "description": "A reference to the data for this resource, as a valid URI string.", + "type": "string", + "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", + "examples": [ + "event/2001/vehicle_locations_2001_01.csv" + ], + "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." + }, + "schema": { + "propertyOrder": 40, + "title": "Table Schema", + "description": "An object describing the TIDES Table Schema for this resource.", + "type": "object", + "required": [ "name", - "path", - "schema" - ], - "properties": { - "profile": { - "enum": [ - "tabular-data-resource" - ], - "propertyOrder": 10, - "title": "Profile", - "description": "Should be `tides-data-package`", - "context": "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.", - "type": "string", - "examples": [ - "{\n \"profile\": \"tides-data-package\"\n}\n", - "{\n \"profile\": \"http://example.com/my-profiles-json-schema.json\"\n}\n" - ] - }, - "name": { - "propertyOrder": 20, - "title": "Name", - "description": "An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.", - "type": "string", - "pattern": "^([-a-z0-9._/])+$", - "context": "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.", - "examples": [ - "{\n \"name\": \"tabular-data-resource\"\n}\n" - ] - }, - "path": { - "propertyOrder": 30, - "title": "Path", - "description": "A reference to the data for this resource, as a valid URI string.", - "type": "string", - "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", - "examples": [ - "{\n \"path\": \"file.csv\"\n}\n", - "{\n \"path\": \"http://example.com/file.csv\"\n}\n" - ], - "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." - }, - "schema": { - "propertyOrder": 40, - "title": "Table Schema", - "description": "A Table Schema for this resource, compliant with one of the [TIDES](/) table specifications.", - "type": "string", - "required": [ - "name", - "path" - ], - "properties": { - "name": { - "propertyOrder": 10, - "title": "Name", - "description": "An identifier string corresponding to the name of one of the table specs in the TIDES data package", - "type": "string", - "pattern": "^([-a-z0-9._/])+$", - "examples": [ - "{\n \"name\": \"fare_transactions\"\n}\n", - "{\n \"name\": \"train_cars\"\n}\n" - ] - }, - "path": { - "propertyOrder": 20, - "title": "Path", - "description": "A fully qualified public URL, or a relative Unix-style file path, leading to a place where the relevant table spec is defined.", - "type": "string", - "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", - "examples": [ - "{\n \"path\": \"/fare_transactions.schema.json\"\n}\n", - "{\n \"path\": \"https://github.com/TIDES-transit/TIDES/blob/main/spec/train_cars.schema.json\"\n}\n" - ] - } - }, - "examples": [ - "{\n \"schema\": {\n \"fields\": [\n {\n \"name\": \"first_name\",\n \"type\": \"string\"\n \"constraints\": {\n \"required\": true\n }\n },\n {\n \"name\": \"age\",\n \"type\": \"integer\"\n },\n ],\n \"primaryKey\": [\n \"name\"\n ]\n }\n}\n" - ] - }, - "title": { - "title": "Title", - "description": "A human-readable title.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ], - "propertyOrder": 50 - }, - "description": { - "title": "Description", - "description": "A text description. Markdown is encouraged.", - "type": "string", - "examples": [ - "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n" - ], - "propertyOrder": 60, - "format": "textarea" - }, - "homepage": { - "propertyOrder": 70, - "title": "Home Page", - "description": "The home on the web that is related to this data package.", - "type": "string", - "format": "uri", - "examples": [ - "{\n \"homepage\": \"http://example.com/\"\n}\n" - ] - }, - "sources": { - "propertyOrder": 140, - "options": { - "hidden": true - }, - "title": "Sources", - "description": "The raw sources for this resource.", - "type": "array", - "minItems": 0, - "items": { - "title": "Source", - "description": "A source file.", - "type": "object", - "required": [ - "title" + "path" + ], + "properties": { + "name": { + "propertyOrder": 10, + "title": "Name", + "enum": [ + "devices", + "fare_transactions", + "operators", + "station_activities", + "passenger_events", + "stop_visits", + "train_cars", + "trips_performed", + "vehicle_locations", + "vehicle_train_cars", + "vehicles" ], - "properties": { - "title": { - "title": "Title", - "description": "Description of the data source.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ] - }, - "component": { - "title": "Component", - "description": "What technology component was used to generate this data (directly or indirectly)? Examples include `AVL`, `APC`, `AFC`, etc.", - "type": "string" - }, - "product": { - "title": "Product", - "description": "What product was used to generate this data (directly or indirectly)?", - "type": "string" - }, - "product_version": { - "title": "Product Version", - "description": "Describe the version of the product was used.", - "type": "string" - }, - "vendor": { - "title": "Vendor", - "description": "What company makes this product?", - "type": "string" - } - } + "description": "An identifier string corresponding to the name of one of the table specs in the TIDES data package", + "type": "string", + "examples": [ + "fare_transactions", + "train_cars" + ] + }, + "path": { + "propertyOrder": 20, + "title": "Path", + "description": "A fully qualified public URL, or a relative Unix-style file path, leading to a place where the relevant table spec is defined.", + "type": "string", + "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", + "examples": [ + "https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/train_cars.schema.json", + "fare_transactions.schema.json" + ] } }, - "licenses": { - "description": "Should be `[{\"name\": \"Apache-2.0\"}]` to be consistent with this repository", - "propertyOrder": 150, - "options": { - "hidden": true - }, - "title": "Licenses", - "type": "array", - "minItems": 1, - "items": { - "title": "License", - "description": "A license for this descriptor.", - "type": "object", - "anyOf": [ - { - "required": [ - "name" - ] - }, - { - "required": [ - "path" - ] - } - ], - "properties": { - "name": { - "title": "Open Definition license identifier", - "description": "MUST be an Open Definition license identifier, see http://licenses.opendefinition.org/", - "type": "string", - "pattern": "^([-a-zA-Z0-9._])+$" - }, - "path": { - "title": "Path", - "description": "A fully qualified public URL, or a relative Unix-style file path.", - "type": "string", - "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", - "examples": [ - "{\n \"path\": \"/license.md\"\n}\n", - "{\n \"path\": \"https://opensource.org/license/apache-2-0/\"\n}\n" - ], - "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." - }, - "title": { - "title": "Title", - "description": "A human-readable title.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ] - } - } - }, - "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", - "examples": [ - "{\n \"licenses\": [\n {\n \"name\": \"odc-pddl-1.0\",\n \"path\": \"http://opendatacommons.org/licenses/pddl/\",\n \"title\": \"Open Data Commons Public Domain Dedication and License v1.0\"\n }\n ]\n}\n" - ] + "examples": [ + "`{'name': 'devices' ,'path': 'https://raw.githubusercontent.com/TIDES-transit/TIDES/main/spec/devices.schema.json'}`" + ] + }, + "title": { + "title": "Title", + "description": "A human-readable title for the data resource.", + "type": "string", + "examples": [ + "Neighborhood Trolley Vehicle Locations: 1/1/2001-2/1/2001" + ], + "propertyOrder": 50 + }, + "description": { + "title": "Description", + "description": "D", + "type": "string", + "examples": [ + "Vehicle location event data from the Neighborhood of Make Believe Trolley recorded 1/1/2001-2/1/2001" + ], + "propertyOrder": 60, + "format": "textarea" + }, + "homepage": { + "propertyOrder": 70, + "title": "Home Page", + "description": "The home on the web that is related to this data package.", + "type": "string", + "format": "uri", + "examples": [ + "http://example.com" + ] + }, + "sources": { + "propertyOrder": 140, + "options": { + "hidden": true }, - "dialect": { - "propertyOrder": 50, - "title": "CSV Dialect", - "description": "The CSV dialect descriptor.", - "type": [ - "string", - "object" - ], + "title": "Sources", + "description": "The raw sources for this resource which describe where the data came from.", + "type": "array", + "minItems": 0, + "examples": [ + "[ {'title':'Trolley CAD/AVL System','component':'CAD/AVL','product':'TrolleyMaster','product_version':'3.1.4'} ]" + ], + "items": { + "title": "Source", + "type": "object", "required": [ - "delimiter", - "doubleQuote" + "title" ], "properties": { - "csvddfVersion": { - "title": "CSV Dialect schema version", - "description": "A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.", - "type": "number", - "default": 1.2, - "examples:": [ - "{\n \"csvddfVersion\": \"1.2\"\n}\n" - ] - }, - "delimiter": { - "title": "Delimiter", - "description": "A character sequence to use as the field separator.", + "title": { + "title": "Title", + "description": "Human-readable title of the data source.", "type": "string", - "default": ",", "examples": [ - "{\n \"delimiter\": \",\"\n}\n", - "{\n \"delimiter\": \";\"\n}\n" + "Trolley CAD/AVL System" ] }, - "doubleQuote": { - "title": "Double Quote", - "description": "Specifies the handling of quotes inside fields.", - "context": "If Double Quote is set to true, two consecutive quotes must be interpreted as one.", - "type": "boolean", - "default": true, - "examples": [ - "{\n \"doubleQuote\": true\n}\n" - ] - }, - "lineTerminator": { - "title": "Line Terminator", - "description": "Specifies the character sequence that must be used to terminate rows.", + "component": { + "title": "Component", + "description": "The technology component used to generate this data (directly or indirectly)", "type": "string", - "default": "\r\n", "examples": [ - "{\n \"lineTerminator\": \"\\r\\n\"\n}\n", - "{\n \"lineTerminator\": \"\\n\"\n}\n" + "CAD/AVL", + "AFC", + "APC" ] }, - "nullSequence": { - "title": "Null Sequence", - "description": "Specifies the null sequence, for example, `\\N`.", + "product": { + "title": "Product", + "description": "What product was used to generate this data (directly or indirectly)?", "type": "string", "examples": [ - "{\n \"nullSequence\": \"\\N\"\n}\n" + "TrolleyMaster" ] }, - "quoteChar": { - "title": "Quote Character", - "description": "Specifies a one-character string to use as the quoting character.", - "type": "string", - "default": "\"", - "examples": [ - "{\n \"quoteChar\": \"'\"\n}\n" - ] - }, - "escapeChar": { - "title": "Escape Character", - "description": "Specifies a one-character string to use as the escape character.", - "type": "string", - "examples": [ - "{\n \"escapeChar\": \"\\\\\"\n}\n" - ] + "product_version": { + "title": "Product Version", + "description": "Describe the version of the product was used.", + "type": "string" }, - "skipInitialSpace": { - "title": "Skip Initial Space", - "description": "Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.", - "type": "boolean", - "default": false, - "examples": [ - "{\n \"skipInitialSpace\": true\n}\n" + "vendor": { + "title": "Vendor", + "description": "What company makes this product?", + "type": "string" + } + } + } + }, + "licenses": { + "propertyOrder": 90, + "title": "Licenses", + "description": "The license(s) under which this package is published.", + "type": "array", + "minItems": 1, + "items": { + "title": "License", + "description": "An array of license objects which apply to this data package. Must include at least a `name` or `path`", + "type": "object", + "anyOf": [ + { + "required": [ + "name" ] }, - "header": { - "title": "Header", - "description": "Specifies if the file includes a header row, always as the first row in the file.", - "type": "boolean", - "default": true, - "examples": [ - "{\n \"header\": true\n}\n" + { + "required": [ + "path" ] + } + ], + "properties": { + "name": { + "title": "Open Definition license identifier", + "description": "MUST be an Open Definition license identifier, see http://licenses.opendefinition.org/", + "type": "string", + "pattern": "^([-a-zA-Z0-9._])+$", + "examples": ["odc-by","CC-BY-4.0"] }, - "commentChar": { - "title": "Comment Character", - "description": "Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.", + "path": { + "title": "Path", + "description": "A fully qualified public URL, or a Unix-style relative file path.", "type": "string", + "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", "examples": [ - "{\n \"commentChar\": \"#\"\n}\n" - ] + "https://opendefinition.org/licenses/odc-by/", + "license.md" + ], + "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." }, - "caseSensitiveHeader": { - "title": "Case Sensitive Header", - "description": "Specifies if the case of headers is meaningful.", - "context": "Use of case in source CSV files is not always an intentional decision. For example, should \"CAT\" and \"Cat\" be considered to have the same meaning.", - "type": "boolean", - "default": false, + "title": { + "title": "Title", + "description": "A human-readable title of the license.", + "type": "string", "examples": [ - "{\n \"caseSensitiveHeader\": true\n}\n" + "Creative Commons Attribution 4.0" ] } - }, - "examples": [ - "{\n \"dialect\": {\n \"delimiter\": \";\"\n }\n}\n", - "{\n \"dialect\": {\n \"delimiter\": \"\\t\",\n \"quoteChar\": \"'\",\n \"commentChar\": \"#\"\n }\n}\n" - ] - }, - "format": { - "propertyOrder": 80, - "title": "Format", - "description": "The file format of this resource.", - "context": "`csv`, `xls`, `json` are examples of common formats.", - "type": "string", - "examples": [ - "{\n \"format\": \"xls\"\n}\n" - ] + } }, - "mediatype": { - "propertyOrder": 90, - "title": "Media Type", - "description": "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).", - "type": "string", - "pattern": "^(.+)/(.+)$", - "examples": [ - "{\n \"mediatype\": \"text/csv\"\n}\n" - ] + "context": "This property is not legally binding and does not guarantee that the package is licensed under the terms defined herein.", + "examples": [ + "{'name':'CC-BY-4.0','path':'https://opendefinition.org/licenses/cc-by/','name':'Creative Commons Attribution 4.0'}" + ] + }, + "dialect": { + "propertyOrder": 50, + "title": "CSV Dialect", + "description": "The CSV dialect descriptor.", + "type": [ + "string", + "object" + ], + "required": [ + "delimiter", + "doubleQuote" + ], + "properties": { + "csvddfVersion": { + "title": "CSV Dialect schema version", + "description": "A number to indicate the schema version of CSV Dialect. Version 1.0 was named CSV Dialect Description Format and used different field names.", + "type": "number", + "default": 1.2, + "examples:": [ + "1.2" + ] + }, + "delimiter": { + "title": "Delimiter", + "description": "A character sequence to use as the field separator.", + "type": "string", + "default": ",", + "examples": [ + "`;`", + "`|`" + ] + }, + "doubleQuote": { + "title": "Double Quote", + "description": "Specifies the handling of quotes inside fields.", + "context": "If Double Quote is set to true, two consecutive quotes must be interpreted as one.", + "type": "boolean", + "default": true + }, + "lineTerminator": { + "title": "Line Terminator", + "description": "Specifies the character sequence that must be used to terminate rows.", + "type": "string", + "default": "\r\n", + "examples": [ + "`\r`", + "`\n`" + ] + }, + "nullSequence": { + "title": "Null Sequence", + "description": "Specifies the null sequence, for example, `\\N`.", + "type": "string", + "examples": [ + "`\\N`" + ] + }, + "quoteChar": { + "title": "Quote Character", + "description": "Specifies a one-character string to use as the quoting character.", + "type": "string", + "default": "\"", + "examples": [ + "`'`" + ] + }, + "escapeChar": { + "title": "Escape Character", + "description": "Specifies a one-character string to use as the escape character.", + "type": "string", + "examples": [ + "`\\`" + ] + }, + "skipInitialSpace": { + "title": "Skip Initial Space", + "description": "Specifies the interpretation of whitespace immediately following a delimiter. If false, whitespace immediately after a delimiter should be treated as part of the subsequent field.", + "type": "boolean", + "default": false + }, + "header": { + "title": "Header", + "description": "Specifies if the file includes a header row, always as the first row in the file.", + "type": "boolean", + "default": true + }, + "commentChar": { + "title": "Comment Character", + "description": "Specifies that any row beginning with this one-character string, without preceeding whitespace, causes the entire line to be ignored.", + "type": "string", + "examples": [ + "`#`" + ] + }, + "caseSensitiveHeader": { + "title": "Case Sensitive Header", + "description": "Specifies if the case of headers is meaningful.", + "context": "Use of case in source CSV files is not always an intentional decision. For example, should \"CAT\" and \"Cat\" be considered to have the same meaning.", + "type": "boolean", + "default": false + } }, - "encoding": { - "propertyOrder": 100, - "title": "Encoding", - "description": "The file encoding of this resource.", - "type": "string", - "default": "utf-8", - "examples": [ - "{\n \"encoding\": \"utf-8\"\n}\n" - ] + "examples": [ + "{'delimiter': ';' }" + ] + }, + "format": { + "propertyOrder": 80, + "title": "Format", + "description": "The file format of this resource.", + "context": "`csv`, `xls`, `json` are examples of common formats.", + "type": "string", + "examples": [ + "`xls`" + ] + }, + "mediatype": { + "propertyOrder": 90, + "title": "Media Type", + "description": "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).", + "type": "string", + "pattern": "^(.+)/(.+)$", + "examples": [ + "`text/csv`" + ] + }, + "encoding": { + "propertyOrder": 100, + "title": "Encoding", + "description": "The file encoding of this resource.", + "type": "string", + "default": "utf-8", + "examples": [ + "`utf-8`" + ] + }, + "bytes": { + "propertyOrder": 110, + "options": { + "hidden": true }, - "bytes": { - "propertyOrder": 110, - "options": { - "hidden": true - }, - "title": "Bytes", - "description": "The size of this resource in bytes.", - "type": "integer", - "examples": [ - "{\n \"bytes\": 2082\n}\n" - ] + "title": "Bytes", + "description": "The size of this resource in bytes.", + "type": "integer", + "examples": [ + "2082" + ] + }, + "hash": { + "propertyOrder": 120, + "options": { + "hidden": true }, - "hash": { - "propertyOrder": 120, - "options": { - "hidden": true - }, - "title": "Hash", - "type": "string", - "description": "The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.", - "pattern": "^([^:]+:[a-fA-F0-9]+|[a-fA-F0-9]{32}|)$", - "examples": [ - "{\n \"hash\": \"d25c9c77f588f5dc32059d2da1136c02\"\n}\n", - "{\n \"hash\": \"SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0\"\n}\n" - ] - } + "title": "Hash", + "type": "string", + "description": "The MD5 hash of this resource. Indicate other hashing algorithms with the {algorithm}:{hash} format.", + "pattern": "^([^:]+:[a-fA-F0-9]+|[a-fA-F0-9]{32}|)$", + "examples": [ + "d25c9c77f588f5dc32059d2da1136c02", + "SHA256:5262f12512590031bbcc9a430452bfd75c2791ad6771320bb4b5728bfb78c4d0" + ] } - }, - "examples": [ - "{\n \"resources\": [\n {\n \"name\": \"my-data\",\n \"data\": [\n \"data.csv\"\n ],\n \"schema\": \"tableschema.json\",\n \"mediatype\": \"text/csv\"\n }\n ]\n}\n" - ] + } + } + }, + "sources": { + "propertyOrder": 140, + "options": { + "hidden": true }, - "sources": { - "propertyOrder": 110, - "options": { - "hidden": true - }, - "title": "Sources", - "description": "Array of data sources formatted as a source. Recommended to be documented either here at the top-level or for each individual resource", - "type": "array", - "minItems": 0, - "items": { - "title": "Source", - "description": "A source file.", - "type": "object", - "required": [ - "title" - ], - "properties": { - "title": { - "title": "Title", - "description": "A human-readable title.", - "type": "string", - "examples": [ - "{\n \"title\": \"My Package Title\"\n}\n" - ] - }, - "path": { - "title": "Path", - "description": "A fully qualified URL, or a POSIX file path.", - "type": "string", - "pattern": "^(?=^[^./~])(^((?!\\.{2}).)*$).*$", - "examples": [ - "{\n \"path\": \"file.csv\"\n}\n", - "{\n \"path\": \"http://example.com/file.csv\"\n}\n" - ], - "context": "Implementations need to negotiate the type of path provided, and dereference the data accordingly." - }, - "email": { - "title": "Email", - "description": "An email address.", - "type": "string", - "format": "email", - "examples": [ - "{\n \"email\": \"example@example.com\"\n}\n" - ] - } + "title": "Sources", + "description": "The raw sources for this data package which describe where the data came from. Can be alternatively specified at the individual resource-level.", + "type": "array", + "minItems": 0, + "examples": [ + "[ {'title':'Trolley CAD/AVL System','component':'CAD/AVL','product':'TrolleyMaster','product_version':'3.1.4'} ]" + ], + "items": { + "title": "Source", + "type": "object", + "required": [ + "title" + ], + "properties": { + "title": { + "title": "Title", + "description": "Human-readable title of the data source.", + "type": "string", + "examples": [ + "Trolley CAD/AVL System" + ] + }, + "component": { + "title": "Component", + "description": "What technology component was used to generate this data (directly or indirectly)? Examples include `AVL`, `APC`, `AFC`, etc.", + "type": "string", + "examples": [ + "CAD/AVL", + "AFC", + "APC" + ] + }, + "product": { + "title": "Product", + "description": "What product was used to generate this data (directly or indirectly)?", + "type": "string", + "examples": [ + "TrolleyMaster" + ] + }, + "product_version": { + "title": "Product Version", + "description": "Describe the version of the product was used.", + "type": "string" + }, + "vendor": { + "title": "Vendor", + "description": "What company makes this product?", + "type": "string" } - }, - "examples": [ - "{\n \"sources\": [\n {\n \"title\": \"World Bank and OECD\",\n \"path\": \"http://data.worldbank.org/indicator/NY.GDP.MKTP.CD\"\n }\n ]\n}\n" - ] + } } } } +}