├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README-cli.md
├── README-dev.md
├── README-py.md
├── README.md
├── VERSION
├── publish.sh
├── requirements-dev.txt
├── requirements-lower-bound.txt
├── requirements.txt
├── setup.cfg
├── setup.py
├── tableschema_to_template
├── __init__.py
├── create_xlsx.py
├── errors.py
├── ts2xl.py
├── validate_schema.py
└── validation_factory.py
├── test-cli.sh
├── test.sh
└── tests
├── fixtures
├── README.md
├── output-unzipped
│ ├── [Content_Types].xml
│ ├── _rels
│ │ └── .rels
│ ├── docProps
│ │ ├── app.xml
│ │ └── core.xml
│ └── xl
│ │ ├── _rels
│ │ └── workbook.xml.rels
│ │ ├── comments1.xml
│ │ ├── drawings
│ │ └── vmlDrawing1.vml
│ │ ├── sharedStrings.xml
│ │ ├── styles.xml
│ │ ├── theme
│ │ └── theme1.xml
│ │ ├── workbook.xml
│ │ └── worksheets
│ │ ├── _rels
│ │ └── sheet1.xml.rels
│ │ ├── sheet1.xml
│ │ ├── sheet2.xml
│ │ └── sheet3.xml
├── schema.yaml
└── template.xlsx
└── test_create_xlsx.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Excel backups:
2 | ~*
3 |
4 | # pypi token:
5 | .pypirc
6 |
7 | # Packaging:
8 | build/
9 | dist/
10 | tableschema_to_template.egg-info/
11 |
12 | **/__pycache__
13 | .vscode/
14 | **/.DS_Store
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '3.8'
4 | - '3.6' # Keep in sync with setup.py
5 | cache: pip
6 | env:
7 | - REQS=requirements.txt
8 | - REQS=requirements-lower-bound.txt
9 | script:
10 | - pip install -r $REQS
11 | - pip install -r requirements-dev.txt
12 | - ./test.sh
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | 0.0.13 - 2023-02-01
2 | - Update publish.sh to include license and prune unneeded files in sdist.
3 | - Update manifest.in and dev dependencies for new build process.
4 |
5 | 0.0.12 - 2020-12-15
6 | - Check for changelog updates with each PR.
7 | - Keep CLI and Python docs in sync.
8 | - Range checks on numbers.
9 |
10 | 0.0.11
11 | - Type and enum validation.
12 | - Python and CLI interfaces.
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Chuck McCallum
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include VERSION
2 | exclude .gitignore
3 | exclude .travis.yml
4 | exclude publish.sh
5 | exclude test-cli.sh
6 | exclude test.sh
7 | exclude .pypirc
8 | prune tests
--------------------------------------------------------------------------------
/README-cli.md:
--------------------------------------------------------------------------------
1 | ```
2 | usage: ts2xl.py [-h] [--sheet_name NAME] [--idempotent] SCHEMA EXCEL
3 |
4 | Given a Frictionless Table Schema, generates an Excel template with input
5 | validation.
6 |
7 | positional arguments:
8 | SCHEMA Path of JSON or YAML Table Schema.
9 | EXCEL Path of Excel file to create. Must end with ".xlsx".
10 |
11 | optional arguments:
12 | -h, --help show this help message and exit
13 | --sheet_name NAME Optionally, specify the name of the data-entry sheet.
14 | --idempotent If set, internal date-stamp is set to 2000-01-01, so re-
15 | runs are identical.
16 | ```
--------------------------------------------------------------------------------
/README-dev.md:
--------------------------------------------------------------------------------
1 | ## Development
2 |
3 | From a checkout of the repo, run a demo:
4 | ```sh
5 | pip install -r requirements.txt
6 | PYTHONPATH="${PYTHONPATH}:tableschema_to_template" \
7 | tableschema_to_template/ts2xl.py \
8 | tests/fixtures/schema.yaml /tmp/template.xlsx
9 | # Open with Excel:
10 | open /tmp/template.xlsx
11 | ```
12 |
13 | Run the tests:
14 | ```sh
15 | pip install -r requirements-dev.txt
16 | ./test.sh
17 | ```
18 |
19 | To build and publish, make sure you have a `.pypirc` with a token,
20 | and then run `./publish.sh`.
--------------------------------------------------------------------------------
/README-py.md:
--------------------------------------------------------------------------------
1 | ```
2 | Help on function create_xlsx in tableschema_to_template:
3 |
4 | tableschema_to_template.create_xlsx = create_xlsx(table_schema, xlsx_path, sheet_name='Export this as TSV', idempotent=False)
5 | Creates Excel file with data validation from a Table Schema.
6 |
7 | Args:
8 | table_schema: Table Schema as dict.
9 | xlsx_path: Path of Excel file to create. Must end with ".xlsx".
10 | sheet_name: Optionally, specify the name of the data-entry sheet.
11 | idempotent: If set, internal date-stamp is set to 2000-01-01, so re-runs are identical.
12 |
13 | Returns:
14 | No return value.
15 |
16 | Raises:
17 | tableschema_to_template.errors.Ts2xlException if table_schema is invalid.
18 |
19 | ```
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tableschema-to-template
2 |
3 | Given a [Frictionless Table Schema](https://specs.frictionlessdata.io/table-schema/)
4 | (like [this](https://raw.githubusercontent.com/hubmapconsortium/tableschema-to-template/main/tests/fixtures/schema.yaml)),
5 | generate an Excel template with input validation
6 | (like [this](https://raw.githubusercontent.com/hubmapconsortium/tableschema-to-template/main/tests/fixtures/template.xlsx)).
7 |
8 | ## Usage
9 |
10 | Download a [sample `schema.yaml`](https://raw.githubusercontent.com/hubmapconsortium/tableschema-to-template/main/tests/fixtures/schema.yaml), and then:
11 |
12 | ```sh
13 | pip install tableschema-to-template
14 | ts2xl.py schema.yaml template.xlsx
15 | # Open with Excel:
16 | open template.xlsx
17 | ```
18 |
19 | Or to use inside Python:
20 | ```python
21 | from tableschema_to_template import create_xlsx
22 | schema = {'fields': [{
23 | 'name': 'a_number',
24 | 'description': 'A number!',
25 | 'type': 'number'
26 | }]}
27 | create_xlsx(schema, '/tmp/template.xlsx')
28 | ```
29 |
30 | Additional docs:
31 | - [For CLI users](https://github.com/hubmapconsortium/tableschema-to-template/blob/main/README-cli.md#readme)
32 | - [For Python users](https://github.com/hubmapconsortium/tableschema-to-template/blob/main/README-py.md#readme)
33 | - [For project developers](https://github.com/hubmapconsortium/tableschema-to-template/blob/main/README-dev.md#readme)
34 |
35 | ## Features
36 |
37 | - Enum constraints transformed into pull-downs.
38 | - Field descriptions transformed into comments in header.
39 | - Float, integer, and boolean type validation, with range checks on numbers.
40 |
41 | More details in the [changelog](https://github.com/hubmapconsortium/tableschema-to-template/blob/main/CHANGELOG.md#readme).
42 |
43 | ## Related work
44 |
45 | If you want to construct Excel files programmatically, [XlsxWriter](https://xlsxwriter.readthedocs.io/) is great!
46 |
47 | For validated data entry, from the Frictionless community:
48 | - [`table-schema-resource-template`](https://pypi.org/project/table-schema-resource-template/): Generates templates, but doesn't go beyond row headers.
49 | - [`data-curator`](https://github.com/qcif/data-curator): Desktop application for data entry based on Table Schema.
50 | - [`csv-gg`](https://github.com/etalab/csv-gg): Web app which serves data entry form, and uses [Validata API](https://git.opendatafrance.net/validata/) for validation.
51 |
52 | From the biomedical ontologies community:
53 | - [`CEDAR`](https://more.metadatacenter.org/): Data entry tool based on ontologies.
54 | - [`Webulous`](https://www.ebi.ac.uk/spot/webulous/): Google sheets plugin that adds pulldowns based on ontology terms.
55 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.0.13
--------------------------------------------------------------------------------
/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -o errexit
3 | set -o pipefail
4 |
5 | red=`tput setaf 1`
6 | green=`tput setaf 2`
7 | reset=`tput sgr0`
8 |
9 | die() { set +v; echo "${red}$*${reset}" 1>&2 ; sleep 1; exit 1; }
10 |
11 | cd `dirname $0`
12 |
13 | git diff --quiet || die 'Uncommitted changes: Stash or commit'
14 | git checkout main
15 | git pull
16 |
17 | perl -i -pne 's/(\d+)$/$1+1/e' VERSION
18 |
19 | rm -rf build/
20 | rm -rf dist/
21 |
22 | python3 -m build
23 | python3 -m twine upload \
24 | --config-file .pypirc \
25 | --non-interactive \
26 | dist/*
27 |
28 | VERSION=`cat VERSION`
29 | git add .
30 | git commit -m "Version $VERSION"
31 | git tag $VERSION
32 | git push origin --tags
33 | git push origin
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | flake8==3.8.4
2 | autopep8==1.5.4
3 | pytest==6.1.1
4 | yattag==1.14.0
5 | twine==3.2.0
6 | build==0.9.0
--------------------------------------------------------------------------------
/requirements-lower-bound.txt:
--------------------------------------------------------------------------------
1 | # Keep this in sync with setup.py.
2 | jsonschema==1.0.0
3 | pyyaml==3.13
4 | xlsxwriter==1.2.8
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Periodically update these to the latest versions.
2 | jsonschema==3.2.0
3 | pyyaml==5.3.1
4 | xlsxwriter==1.3.7
5 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 99
3 | exclude = build
4 | ignore =
5 | W503
6 | # "line break before binary operator":
7 | # Prefer operator at start of line so the context is clear.
8 | per-file-ignores =
9 | tableschema_to_template/validate_schema.py:E501
10 | # Ignore line length in pasted JSON.
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | with open("VERSION", "r") as fh:
7 | version = fh.read()
8 |
9 | setuptools.setup(
10 | name="tableschema-to-template",
11 | version=version,
12 | install_requires=[
13 | # Keep in sync with requirements-lower-bound.txt:
14 | 'jsonschema>=1.0.0',
15 | 'pyyaml>=3.13',
16 | 'xlsxwriter>=1.2.8'
17 | # xlsxwriter bound could be loosened:
18 | # Earlier versions generate slightly different XML, and tests fail here,
19 | # but that's only because they are too fussy.
20 | ],
21 | scripts=[
22 | 'tableschema_to_template/ts2xl.py'
23 | ],
24 | author="Chuck McCallum",
25 | author_email="mccallucc+tableschema@gmail.com",
26 | description="Given a Frictionless Table Schema, "
27 | "generates an Excel template with input validation",
28 | long_description=long_description,
29 | long_description_content_type="text/markdown",
30 | url="https://github.com/hubmapconsortium/tableschema-to-template",
31 | packages=setuptools.find_packages(),
32 | classifiers=[
33 | "Programming Language :: Python :: 3",
34 | "License :: OSI Approved :: MIT License",
35 | "Operating System :: OS Independent",
36 | ],
37 | # Keep in sync with .travis.yml:
38 | python_requires='>=3.6',
39 | # f-strings aren't available in 3.5.
40 | # pyyaml install fails on 3.4.
41 | )
42 |
--------------------------------------------------------------------------------
/tableschema_to_template/__init__.py:
--------------------------------------------------------------------------------
1 | # Export from the top level:
2 | from tableschema_to_template.create_xlsx import create_xlsx # noqa: F401
3 |
--------------------------------------------------------------------------------
/tableschema_to_template/create_xlsx.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from xlsxwriter import Workbook
4 | from xlsxwriter.utility import xl_col_to_name
5 |
6 | from tableschema_to_template.validation_factory import get_validation
7 | from tableschema_to_template.validate_schema import validate_schema
8 |
9 |
10 | def _col_below_header(i):
11 | col_name = xl_col_to_name(i)
12 | row_max = 1048576
13 | return f'{col_name}2:{col_name}{row_max}'
14 |
15 |
16 | def create_xlsx(
17 | table_schema, xlsx_path,
18 | sheet_name='Export this as TSV',
19 | idempotent=False
20 | ):
21 | '''
22 | Creates Excel file with data validation from a Table Schema.
23 |
24 | Args:
25 | table_schema: Table Schema as dict.
26 | xlsx_path: Path of Excel file to create. Must end with ".xlsx".
27 | sheet_name: Optionally, specify the name of the data-entry sheet.
28 | idempotent: If set, internal date-stamp is set to 2000-01-01, so re-runs are identical.
29 |
30 | Returns:
31 | No return value.
32 |
33 | Raises:
34 | tableschema_to_template.errors.Ts2xlException if table_schema is invalid.
35 | '''
36 | validate_schema(table_schema)
37 | workbook = Workbook(xlsx_path)
38 | if idempotent:
39 | workbook.set_properties({
40 | 'created': datetime(2000, 1, 1)
41 | })
42 | main_sheet = workbook.add_worksheet(sheet_name)
43 | main_sheet.freeze_panes(1, 0)
44 |
45 | header_format = workbook.add_format({
46 | 'bold': True,
47 | 'text_wrap': True,
48 | 'align': 'center'
49 | })
50 |
51 | for i, field in enumerate(table_schema['fields']):
52 | main_sheet.write(0, i, field['name'], header_format)
53 | main_sheet.write_comment(0, i, field['description'])
54 | data_validation = get_validation(field, workbook).get_data_validation()
55 | main_sheet.data_validation(_col_below_header(i), data_validation)
56 |
57 | workbook.close()
58 |
--------------------------------------------------------------------------------
/tableschema_to_template/errors.py:
--------------------------------------------------------------------------------
1 | class Ts2xlException(Exception):
2 | '''
3 | Used for expected errors, where the command-line tool
4 | shouldn't show a full stack trace.
5 | '''
6 | pass
7 |
--------------------------------------------------------------------------------
/tableschema_to_template/ts2xl.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import argparse
4 | import sys
5 | import os
6 | import re
7 |
8 | from yaml import safe_load
9 |
10 | from tableschema_to_template.errors import Ts2xlException
11 | from tableschema_to_template import create_xlsx
12 |
13 |
14 | def _xlsx_path(s):
15 | if os.path.exists(s):
16 | raise Ts2xlException(f'"{s}" already exists')
17 | if not s.endswith('.xlsx'):
18 | raise Ts2xlException(f'"{s}" does not end with ".xlsx"')
19 | return s
20 |
21 |
22 | def _make_parser():
23 | parser = argparse.ArgumentParser(
24 | description='''
25 | Given a Frictionless Table Schema,
26 | generates an Excel template with input validation.
27 | ''')
28 | doc_dict = _doc_to_dict(create_xlsx.__doc__)
29 | parser.add_argument(
30 | 'schema_path', type=argparse.FileType('r'),
31 | metavar='SCHEMA',
32 | help='Path of JSON or YAML Table Schema.')
33 | parser.add_argument(
34 | 'xlsx_path', type=_xlsx_path,
35 | metavar='EXCEL',
36 | help=doc_dict['xlsx_path'])
37 | parser.add_argument(
38 | '--sheet_name',
39 | metavar='NAME',
40 | help=doc_dict['sheet_name'])
41 | parser.add_argument(
42 | '--idempotent',
43 | action='store_true',
44 | help=doc_dict['idempotent'])
45 | return parser
46 |
47 |
48 | def _doc_to_dict(doc):
49 | '''
50 | Given google style docs, parse out the arguments,
51 | and return a dict.
52 |
53 | >>> _doc_to_dict('... Args: fake_arg: It works! Returns: ...')
54 | {'fake_arg': 'It works!'}
55 | '''
56 | arg_lines = re.search(
57 | r'(?<=Args:)(.+)(?=Returns:)', doc,
58 | flags=re.DOTALL
59 | ).group(0).strip().split('\n')
60 | arg_matches = [
61 | re.match(r'^\s*(\w+):\s+(\S.*)', arg.strip())
62 | for arg in arg_lines
63 | ]
64 | return {m.group(1): m.group(2) for m in arg_matches}
65 |
66 |
67 | # We want the error handling inside the __name__ == '__main__' section
68 | # to be able to show the usage string if it catches a Ts2xlException.
69 | # Defining this at the top level makes that possible.
70 | _parser = _make_parser()
71 |
72 |
73 | def main():
74 | args = vars(_parser.parse_args())
75 | schema_path = args.pop('schema_path')
76 | table_schema = safe_load(schema_path.read())
77 | xlsx_path = args.pop('xlsx_path')
78 | create_xlsx(table_schema, xlsx_path, **args)
79 |
80 | print(f'Created {xlsx_path}', file=sys.stderr)
81 | return 0
82 |
83 |
84 | if __name__ == "__main__":
85 | try:
86 | exit_status = main()
87 | except Ts2xlException as e:
88 | print(_parser.format_usage(), file=sys.stderr)
89 | print(e, file=sys.stderr)
90 | exit_status = 2
91 | sys.exit(exit_status)
92 |
--------------------------------------------------------------------------------
/tableschema_to_template/validate_schema.py:
--------------------------------------------------------------------------------
1 | from yaml import safe_load
2 | from jsonschema import validate
3 | from jsonschema import ValidationError
4 |
5 | from tableschema_to_template.errors import Ts2xlException
6 |
7 |
8 | def validate_schema(table_schema):
9 | '''
10 | >>> validate_schema({})
11 | Traceback (most recent call last):
12 | ...
13 | tableschema_to_template.errors.Ts2xlException: Not a valid Table Schema: 'fields' ... required property
14 |
15 | (Phrasing of error message changed between versions.)
16 | '''
17 | table_schema_schema = safe_load(_table_schema_schema)
18 | try:
19 | validate(table_schema, table_schema_schema)
20 | except ValidationError as e:
21 | raise Ts2xlException(f'Not a valid Table Schema: {e.message}')
22 |
23 |
24 | # This is ugly, but it's less configuration than including JSON in the build.
25 | # From: https://specs.frictionlessdata.io/schemas/table-schema.json
26 | _table_schema_schema = r'''
27 | {
28 | "$schema": "http://json-schema.org/draft-04/schema#",
29 | "title": "Table Schema",
30 | "description": "A Table Schema for this resource, compliant with the [Table Schema](/tableschema/) specification.",
31 | "type": "object",
32 | "required": [
33 | "fields"
34 | ],
35 | "properties": {
36 | "fields": {
37 | "type": "array",
38 | "minItems": 1,
39 | "items": {
40 | "title": "Table Schema Field",
41 | "type": "object",
42 | "anyOf": [
43 | {
44 | "type": "object",
45 | "title": "String Field",
46 | "description": "The field contains strings, that is, sequences of characters.",
47 | "required": [
48 | "name"
49 | ],
50 | "properties": {
51 | "name": {
52 | "title": "Name",
53 | "description": "A name for this field.",
54 | "type": "string"
55 | },
56 | "title": {
57 | "title": "Title",
58 | "description": "A human-readable title.",
59 | "type": "string",
60 | "examples": [
61 | "{\n \"title\": \"My Package Title\"\n}\n"
62 | ]
63 | },
64 | "description": {
65 | "title": "Description",
66 | "description": "A text description. Markdown is encouraged.",
67 | "type": "string",
68 | "examples": [
69 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
70 | ]
71 | },
72 | "type": {
73 | "description": "The type keyword, which `MUST` be a value of `string`.",
74 | "enum": [
75 | "string"
76 | ]
77 | },
78 | "format": {
79 | "description": "The format keyword options for `string` are `default`, `email`, `uri`, `binary`, and `uuid`.",
80 | "context": "The following `format` options are supported:\n * **default**: any valid string.\n * **email**: A valid email address.\n * **uri**: A valid URI.\n * **binary**: A base64 encoded string representing binary data.\n * **uuid**: A string that is a uuid.",
81 | "enum": [
82 | "default",
83 | "email",
84 | "uri",
85 | "binary",
86 | "uuid"
87 | ],
88 | "default": "default"
89 | },
90 | "constraints": {
91 | "title": "Constraints",
92 | "description": "The following constraints are supported for `string` fields.",
93 | "type": "object",
94 | "properties": {
95 | "required": {
96 | "type": "boolean",
97 | "description": "Indicates whether a property must have a value for each instance.",
98 | "context": "An empty string is considered to be a missing value."
99 | },
100 | "unique": {
101 | "type": "boolean",
102 | "description": "When `true`, each value for the property `MUST` be unique."
103 | },
104 | "pattern": {
105 | "type": "string",
106 | "description": "A regular expression pattern to test each value of the property against, where a truthy response indicates validity.",
107 | "context": "Regular expressions `SHOULD` conform to the [XML Schema regular expression syntax](http://www.w3.org/TR/xmlschema-2/#regexs)."
108 | },
109 | "enum": {
110 | "type": "array",
111 | "minItems": 1,
112 | "uniqueItems": true,
113 | "items": {
114 | "type": "string"
115 | }
116 | },
117 | "minLength": {
118 | "type": "integer",
119 | "description": "An integer that specifies the minimum length of a value."
120 | },
121 | "maxLength": {
122 | "type": "integer",
123 | "description": "An integer that specifies the maximum length of a value."
124 | }
125 | }
126 | },
127 | "rdfType": {
128 | "type": "string",
129 | "description": "The RDF type for this field."
130 | }
131 | },
132 | "examples": [
133 | "{\n \"name\": \"name\",\n \"type\": \"string\"\n}\n",
134 | "{\n \"name\": \"name\",\n \"type\": \"string\",\n \"format\": \"email\"\n}\n",
135 | "{\n \"name\": \"name\",\n \"type\": \"string\",\n \"constraints\": {\n \"minLength\": 3,\n \"maxLength\": 35\n }\n}\n"
136 | ]
137 | },
138 | {
139 | "type": "object",
140 | "title": "Number Field",
141 | "description": "The field contains numbers of any kind including decimals.",
142 | "context": "The lexical formatting follows that of decimal in [XMLSchema](https://www.w3.org/TR/xmlschema-2/#decimal): a non-empty finite-length sequence of decimal digits separated by a period as a decimal indicator. An optional leading sign is allowed. If the sign is omitted, '+' is assumed. Leading and trailing zeroes are optional. If the fractional part is zero, the period and following zero(es) can be omitted. For example: '-1.23', '12678967.543233', '+100000.00', '210'.\n\nThe following special string values are permitted (case does not need to be respected):\n - NaN: not a number\n - INF: positive infinity\n - -INF: negative infinity\n\nA number `MAY` also have a trailing:\n - exponent: this `MUST` consist of an E followed by an optional + or - sign followed by one or more decimal digits (0-9)\n - percentage: the percentage sign: `%`. In conversion percentages should be divided by 100.\n\nIf both exponent and percentages are present the percentage `MUST` follow the exponent e.g. '53E10%' (equals 5.3).",
143 | "required": [
144 | "name"
145 | ],
146 | "properties": {
147 | "name": {
148 | "title": "Name",
149 | "description": "A name for this field.",
150 | "type": "string"
151 | },
152 | "title": {
153 | "title": "Title",
154 | "description": "A human-readable title.",
155 | "type": "string",
156 | "examples": [
157 | "{\n \"title\": \"My Package Title\"\n}\n"
158 | ]
159 | },
160 | "description": {
161 | "title": "Description",
162 | "description": "A text description. Markdown is encouraged.",
163 | "type": "string",
164 | "examples": [
165 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
166 | ]
167 | },
168 | "type": {
169 | "description": "The type keyword, which `MUST` be a value of `number`.",
170 | "enum": [
171 | "number"
172 | ]
173 | },
174 | "format": {
175 | "description": "There are no format keyword options for `number`: only `default` is allowed.",
176 | "enum": [
177 | "default"
178 | ],
179 | "default": "default"
180 | },
181 | "bareNumber": {
182 | "type": "boolean",
183 | "title": "bareNumber",
184 | "description": "a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `€95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.",
185 | "default": true
186 | },
187 | "decimalChar": {
188 | "type": "string",
189 | "description": "A string whose value is used to represent a decimal point within the number. The default value is `.`."
190 | },
191 | "groupChar": {
192 | "type": "string",
193 | "description": "A string whose value is used to group digits within the number. The default value is `null`. A common value is `,` e.g. '100,000'."
194 | },
195 | "constraints": {
196 | "title": "Constraints",
197 | "description": "The following constraints are supported for `number` fields.",
198 | "type": "object",
199 | "properties": {
200 | "required": {
201 | "type": "boolean",
202 | "description": "Indicates whether a property must have a value for each instance.",
203 | "context": "An empty string is considered to be a missing value."
204 | },
205 | "unique": {
206 | "type": "boolean",
207 | "description": "When `true`, each value for the property `MUST` be unique."
208 | },
209 | "enum": {
210 | "oneOf": [
211 | {
212 | "type": "array",
213 | "minItems": 1,
214 | "uniqueItems": true,
215 | "items": {
216 | "type": "string"
217 | }
218 | },
219 | {
220 | "type": "array",
221 | "minItems": 1,
222 | "uniqueItems": true,
223 | "items": {
224 | "type": "number"
225 | }
226 | }
227 | ]
228 | },
229 | "minimum": {
230 | "oneOf": [
231 | {
232 | "type": "string"
233 | },
234 | {
235 | "type": "number"
236 | }
237 | ]
238 | },
239 | "maximum": {
240 | "oneOf": [
241 | {
242 | "type": "string"
243 | },
244 | {
245 | "type": "number"
246 | }
247 | ]
248 | }
249 | }
250 | },
251 | "rdfType": {
252 | "type": "string",
253 | "description": "The RDF type for this field."
254 | }
255 | },
256 | "examples": [
257 | "{\n \"name\": \"field-name\",\n \"type\": \"number\"\n}\n",
258 | "{\n \"name\": \"field-name\",\n \"type\": \"number\",\n \"constraints\": {\n \"enum\": [ \"1.00\", \"1.50\", \"2.00\" ]\n }\n}\n"
259 | ]
260 | },
261 | {
262 | "type": "object",
263 | "title": "Integer Field",
264 | "description": "The field contains integers - that is whole numbers.",
265 | "context": "Integer values are indicated in the standard way for any valid integer.",
266 | "required": [
267 | "name",
268 | "type"
269 | ],
270 | "properties": {
271 | "name": {
272 | "title": "Name",
273 | "description": "A name for this field.",
274 | "type": "string"
275 | },
276 | "title": {
277 | "title": "Title",
278 | "description": "A human-readable title.",
279 | "type": "string",
280 | "examples": [
281 | "{\n \"title\": \"My Package Title\"\n}\n"
282 | ]
283 | },
284 | "description": {
285 | "title": "Description",
286 | "description": "A text description. Markdown is encouraged.",
287 | "type": "string",
288 | "examples": [
289 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
290 | ]
291 | },
292 | "type": {
293 | "description": "The type keyword, which `MUST` be a value of `integer`.",
294 | "enum": [
295 | "integer"
296 | ]
297 | },
298 | "format": {
299 | "description": "There are no format keyword options for `integer`: only `default` is allowed.",
300 | "enum": [
301 | "default"
302 | ],
303 | "default": "default"
304 | },
305 | "bareNumber": {
306 | "type": "boolean",
307 | "title": "bareNumber",
308 | "description": "a boolean field with a default of `true`. If `true` the physical contents of this field must follow the formatting constraints already set out. If `false` the contents of this field may contain leading and/or trailing non-numeric characters (which implementors MUST therefore strip). The purpose of `bareNumber` is to allow publishers to publish numeric data that contains trailing characters such as percentages e.g. `95%` or leading characters such as currencies e.g. `€95` or `EUR 95`. Note that it is entirely up to implementors what, if anything, they do with stripped text.",
309 | "default": true
310 | },
311 | "constraints": {
312 | "title": "Constraints",
313 | "description": "The following constraints are supported for `integer` fields.",
314 | "type": "object",
315 | "properties": {
316 | "required": {
317 | "type": "boolean",
318 | "description": "Indicates whether a property must have a value for each instance.",
319 | "context": "An empty string is considered to be a missing value."
320 | },
321 | "unique": {
322 | "type": "boolean",
323 | "description": "When `true`, each value for the property `MUST` be unique."
324 | },
325 | "enum": {
326 | "oneOf": [
327 | {
328 | "type": "array",
329 | "minItems": 1,
330 | "uniqueItems": true,
331 | "items": {
332 | "type": "string"
333 | }
334 | },
335 | {
336 | "type": "array",
337 | "minItems": 1,
338 | "uniqueItems": true,
339 | "items": {
340 | "type": "integer"
341 | }
342 | }
343 | ]
344 | },
345 | "minimum": {
346 | "oneOf": [
347 | {
348 | "type": "string"
349 | },
350 | {
351 | "type": "integer"
352 | }
353 | ]
354 | },
355 | "maximum": {
356 | "oneOf": [
357 | {
358 | "type": "string"
359 | },
360 | {
361 | "type": "integer"
362 | }
363 | ]
364 | }
365 | }
366 | },
367 | "rdfType": {
368 | "type": "string",
369 | "description": "The RDF type for this field."
370 | }
371 | },
372 | "examples": [
373 | "{\n \"name\": \"age\",\n \"type\": \"integer\",\n \"constraints\": {\n \"unique\": true,\n \"minimum\": 100,\n \"maximum\": 9999\n }\n}\n"
374 | ]
375 | },
376 | {
377 | "type": "object",
378 | "title": "Date Field",
379 | "description": "The field contains temporal date values.",
380 | "required": [
381 | "name",
382 | "type"
383 | ],
384 | "properties": {
385 | "name": {
386 | "title": "Name",
387 | "description": "A name for this field.",
388 | "type": "string"
389 | },
390 | "title": {
391 | "title": "Title",
392 | "description": "A human-readable title.",
393 | "type": "string",
394 | "examples": [
395 | "{\n \"title\": \"My Package Title\"\n}\n"
396 | ]
397 | },
398 | "description": {
399 | "title": "Description",
400 | "description": "A text description. Markdown is encouraged.",
401 | "type": "string",
402 | "examples": [
403 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
404 | ]
405 | },
406 | "type": {
407 | "description": "The type keyword, which `MUST` be a value of `date`.",
408 | "enum": [
409 | "date"
410 | ]
411 | },
412 | "format": {
413 | "description": "The format keyword options for `date` are `default`, `any`, and `{PATTERN}`.",
414 | "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string of YYYY-MM-DD.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).",
415 | "default": "default"
416 | },
417 | "constraints": {
418 | "title": "Constraints",
419 | "description": "The following constraints are supported for `date` fields.",
420 | "type": "object",
421 | "properties": {
422 | "required": {
423 | "type": "boolean",
424 | "description": "Indicates whether a property must have a value for each instance.",
425 | "context": "An empty string is considered to be a missing value."
426 | },
427 | "unique": {
428 | "type": "boolean",
429 | "description": "When `true`, each value for the property `MUST` be unique."
430 | },
431 | "enum": {
432 | "type": "array",
433 | "minItems": 1,
434 | "uniqueItems": true,
435 | "items": {
436 | "type": "string"
437 | }
438 | },
439 | "minimum": {
440 | "type": "string"
441 | },
442 | "maximum": {
443 | "type": "string"
444 | }
445 | }
446 | },
447 | "rdfType": {
448 | "type": "string",
449 | "description": "The RDF type for this field."
450 | }
451 | },
452 | "examples": [
453 | "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\"\n}\n",
454 | "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\",\n \"constraints\": {\n \"minimum\": \"01-01-1900\"\n }\n}\n",
455 | "{\n \"name\": \"date_of_birth\",\n \"type\": \"date\",\n \"format\": \"MM-DD-YYYY\"\n}\n"
456 | ]
457 | },
458 | {
459 | "type": "object",
460 | "title": "Time Field",
461 | "description": "The field contains temporal time values.",
462 | "required": [
463 | "name",
464 | "type"
465 | ],
466 | "properties": {
467 | "name": {
468 | "title": "Name",
469 | "description": "A name for this field.",
470 | "type": "string"
471 | },
472 | "title": {
473 | "title": "Title",
474 | "description": "A human-readable title.",
475 | "type": "string",
476 | "examples": [
477 | "{\n \"title\": \"My Package Title\"\n}\n"
478 | ]
479 | },
480 | "description": {
481 | "title": "Description",
482 | "description": "A text description. Markdown is encouraged.",
483 | "type": "string",
484 | "examples": [
485 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
486 | ]
487 | },
488 | "type": {
489 | "description": "The type keyword, which `MUST` be a value of `time`.",
490 | "enum": [
491 | "time"
492 | ]
493 | },
494 | "format": {
495 | "description": "The format keyword options for `time` are `default`, `any`, and `{PATTERN}`.",
496 | "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string for time.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).",
497 | "default": "default"
498 | },
499 | "constraints": {
500 | "title": "Constraints",
501 | "description": "The following constraints are supported for `time` fields.",
502 | "type": "object",
503 | "properties": {
504 | "required": {
505 | "type": "boolean",
506 | "description": "Indicates whether a property must have a value for each instance.",
507 | "context": "An empty string is considered to be a missing value."
508 | },
509 | "unique": {
510 | "type": "boolean",
511 | "description": "When `true`, each value for the property `MUST` be unique."
512 | },
513 | "enum": {
514 | "type": "array",
515 | "minItems": 1,
516 | "uniqueItems": true,
517 | "items": {
518 | "type": "string"
519 | }
520 | },
521 | "minimum": {
522 | "type": "string"
523 | },
524 | "maximum": {
525 | "type": "string"
526 | }
527 | }
528 | },
529 | "rdfType": {
530 | "type": "string",
531 | "description": "The RDF type for this field."
532 | }
533 | },
534 | "examples": [
535 | "{\n \"name\": \"appointment_start\",\n \"type\": \"time\"\n}\n",
536 | "{\n \"name\": \"appointment_start\",\n \"type\": \"time\",\n \"format\": \"any\"\n}\n"
537 | ]
538 | },
539 | {
540 | "type": "object",
541 | "title": "Date Time Field",
542 | "description": "The field contains temporal datetime values.",
543 | "required": [
544 | "name",
545 | "type"
546 | ],
547 | "properties": {
548 | "name": {
549 | "title": "Name",
550 | "description": "A name for this field.",
551 | "type": "string"
552 | },
553 | "title": {
554 | "title": "Title",
555 | "description": "A human-readable title.",
556 | "type": "string",
557 | "examples": [
558 | "{\n \"title\": \"My Package Title\"\n}\n"
559 | ]
560 | },
561 | "description": {
562 | "title": "Description",
563 | "description": "A text description. Markdown is encouraged.",
564 | "type": "string",
565 | "examples": [
566 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
567 | ]
568 | },
569 | "type": {
570 | "description": "The type keyword, which `MUST` be a value of `datetime`.",
571 | "enum": [
572 | "datetime"
573 | ]
574 | },
575 | "format": {
576 | "description": "The format keyword options for `datetime` are `default`, `any`, and `{PATTERN}`.",
577 | "context": "The following `format` options are supported:\n * **default**: An ISO8601 format string for datetime.\n * **any**: Any parsable representation of a date. The implementing library can attempt to parse the datetime via a range of strategies.\n * **{PATTERN}**: The value can be parsed according to `{PATTERN}`, which `MUST` follow the date formatting syntax of C / Python [strftime](http://strftime.org/).",
578 | "default": "default"
579 | },
580 | "constraints": {
581 | "title": "Constraints",
582 | "description": "The following constraints are supported for `datetime` fields.",
583 | "type": "object",
584 | "properties": {
585 | "required": {
586 | "type": "boolean",
587 | "description": "Indicates whether a property must have a value for each instance.",
588 | "context": "An empty string is considered to be a missing value."
589 | },
590 | "unique": {
591 | "type": "boolean",
592 | "description": "When `true`, each value for the property `MUST` be unique."
593 | },
594 | "enum": {
595 | "type": "array",
596 | "minItems": 1,
597 | "uniqueItems": true,
598 | "items": {
599 | "type": "string"
600 | }
601 | },
602 | "minimum": {
603 | "type": "string"
604 | },
605 | "maximum": {
606 | "type": "string"
607 | }
608 | }
609 | },
610 | "rdfType": {
611 | "type": "string",
612 | "description": "The RDF type for this field."
613 | }
614 | },
615 | "examples": [
616 | "{\n \"name\": \"timestamp\",\n \"type\": \"datetime\"\n}\n",
617 | "{\n \"name\": \"timestamp\",\n \"type\": \"datetime\",\n \"format\": \"default\"\n}\n"
618 | ]
619 | },
620 | {
621 | "type": "object",
622 | "title": "Year Field",
623 | "description": "A calendar year, being an integer with 4 digits. Equivalent to [gYear in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYear)",
624 | "required": [
625 | "name",
626 | "type"
627 | ],
628 | "properties": {
629 | "name": {
630 | "title": "Name",
631 | "description": "A name for this field.",
632 | "type": "string"
633 | },
634 | "title": {
635 | "title": "Title",
636 | "description": "A human-readable title.",
637 | "type": "string",
638 | "examples": [
639 | "{\n \"title\": \"My Package Title\"\n}\n"
640 | ]
641 | },
642 | "description": {
643 | "title": "Description",
644 | "description": "A text description. Markdown is encouraged.",
645 | "type": "string",
646 | "examples": [
647 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
648 | ]
649 | },
650 | "type": {
651 | "description": "The type keyword, which `MUST` be a value of `year`.",
652 | "enum": [
653 | "year"
654 | ]
655 | },
656 | "format": {
657 | "description": "There are no format keyword options for `year`: only `default` is allowed.",
658 | "enum": [
659 | "default"
660 | ],
661 | "default": "default"
662 | },
663 | "constraints": {
664 | "title": "Constraints",
665 | "description": "The following constraints are supported for `year` fields.",
666 | "type": "object",
667 | "properties": {
668 | "required": {
669 | "type": "boolean",
670 | "description": "Indicates whether a property must have a value for each instance.",
671 | "context": "An empty string is considered to be a missing value."
672 | },
673 | "unique": {
674 | "type": "boolean",
675 | "description": "When `true`, each value for the property `MUST` be unique."
676 | },
677 | "enum": {
678 | "oneOf": [
679 | {
680 | "type": "array",
681 | "minItems": 1,
682 | "uniqueItems": true,
683 | "items": {
684 | "type": "string"
685 | }
686 | },
687 | {
688 | "type": "array",
689 | "minItems": 1,
690 | "uniqueItems": true,
691 | "items": {
692 | "type": "integer"
693 | }
694 | }
695 | ]
696 | },
697 | "minimum": {
698 | "oneOf": [
699 | {
700 | "type": "string"
701 | },
702 | {
703 | "type": "integer"
704 | }
705 | ]
706 | },
707 | "maximum": {
708 | "oneOf": [
709 | {
710 | "type": "string"
711 | },
712 | {
713 | "type": "integer"
714 | }
715 | ]
716 | }
717 | }
718 | },
719 | "rdfType": {
720 | "type": "string",
721 | "description": "The RDF type for this field."
722 | }
723 | },
724 | "examples": [
725 | "{\n \"name\": \"year\",\n \"type\": \"year\"\n}\n",
726 | "{\n \"name\": \"year\",\n \"type\": \"year\",\n \"constraints\": {\n \"minimum\": 1970,\n \"maximum\": 2003\n }\n}\n"
727 | ]
728 | },
729 | {
730 | "type": "object",
731 | "title": "Year Month Field",
732 | "description": "A calendar year month, being an integer with 1 or 2 digits. Equivalent to [gYearMonth in XML Schema](https://www.w3.org/TR/xmlschema-2/#gYearMonth)",
733 | "required": [
734 | "name",
735 | "type"
736 | ],
737 | "properties": {
738 | "name": {
739 | "title": "Name",
740 | "description": "A name for this field.",
741 | "type": "string"
742 | },
743 | "title": {
744 | "title": "Title",
745 | "description": "A human-readable title.",
746 | "type": "string",
747 | "examples": [
748 | "{\n \"title\": \"My Package Title\"\n}\n"
749 | ]
750 | },
751 | "description": {
752 | "title": "Description",
753 | "description": "A text description. Markdown is encouraged.",
754 | "type": "string",
755 | "examples": [
756 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
757 | ]
758 | },
759 | "type": {
760 | "description": "The type keyword, which `MUST` be a value of `yearmonth`.",
761 | "enum": [
762 | "yearmonth"
763 | ]
764 | },
765 | "format": {
766 | "description": "There are no format keyword options for `yearmonth`: only `default` is allowed.",
767 | "enum": [
768 | "default"
769 | ],
770 | "default": "default"
771 | },
772 | "constraints": {
773 | "title": "Constraints",
774 | "description": "The following constraints are supported for `yearmonth` fields.",
775 | "type": "object",
776 | "properties": {
777 | "required": {
778 | "type": "boolean",
779 | "description": "Indicates whether a property must have a value for each instance.",
780 | "context": "An empty string is considered to be a missing value."
781 | },
782 | "unique": {
783 | "type": "boolean",
784 | "description": "When `true`, each value for the property `MUST` be unique."
785 | },
786 | "enum": {
787 | "type": "array",
788 | "minItems": 1,
789 | "uniqueItems": true,
790 | "items": {
791 | "type": "string"
792 | }
793 | },
794 | "minimum": {
795 | "type": "string"
796 | },
797 | "maximum": {
798 | "type": "string"
799 | }
800 | }
801 | },
802 | "rdfType": {
803 | "type": "string",
804 | "description": "The RDF type for this field."
805 | }
806 | },
807 | "examples": [
808 | "{\n \"name\": \"month\",\n \"type\": \"yearmonth\"\n}\n",
809 | "{\n \"name\": \"month\",\n \"type\": \"yearmonth\",\n \"constraints\": {\n \"minimum\": 1,\n \"maximum\": 6\n }\n}\n"
810 | ]
811 | },
812 | {
813 | "type": "object",
814 | "title": "Boolean Field",
815 | "description": "The field contains boolean (true/false) data.",
816 | "required": [
817 | "name",
818 | "type"
819 | ],
820 | "properties": {
821 | "name": {
822 | "title": "Name",
823 | "description": "A name for this field.",
824 | "type": "string"
825 | },
826 | "title": {
827 | "title": "Title",
828 | "description": "A human-readable title.",
829 | "type": "string",
830 | "examples": [
831 | "{\n \"title\": \"My Package Title\"\n}\n"
832 | ]
833 | },
834 | "description": {
835 | "title": "Description",
836 | "description": "A text description. Markdown is encouraged.",
837 | "type": "string",
838 | "examples": [
839 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
840 | ]
841 | },
842 | "type": {
843 | "description": "The type keyword, which `MUST` be a value of `boolean`.",
844 | "enum": [
845 | "boolean"
846 | ]
847 | },
848 | "trueValues": {
849 | "type": "array",
850 | "minItems": 1,
851 | "items": {
852 | "type": "string"
853 | },
854 | "default": [
855 | "true",
856 | "True",
857 | "TRUE",
858 | "1"
859 | ]
860 | },
861 | "falseValues": {
862 | "type": "array",
863 | "minItems": 1,
864 | "items": {
865 | "type": "string"
866 | },
867 | "default": [
868 | "false",
869 | "False",
870 | "FALSE",
871 | "0"
872 | ]
873 | },
874 | "constraints": {
875 | "title": "Constraints",
876 | "description": "The following constraints are supported for `boolean` fields.",
877 | "type": "object",
878 | "properties": {
879 | "required": {
880 | "type": "boolean",
881 | "description": "Indicates whether a property must have a value for each instance.",
882 | "context": "An empty string is considered to be a missing value."
883 | },
884 | "enum": {
885 | "type": "array",
886 | "minItems": 1,
887 | "uniqueItems": true,
888 | "items": {
889 | "type": "boolean"
890 | }
891 | }
892 | }
893 | },
894 | "rdfType": {
895 | "type": "string",
896 | "description": "The RDF type for this field."
897 | }
898 | },
899 | "examples": [
900 | "{\n \"name\": \"registered\",\n \"type\": \"boolean\"\n}\n"
901 | ]
902 | },
903 | {
904 | "type": "object",
905 | "title": "Object Field",
906 | "description": "The field contains data which can be parsed as a valid JSON object.",
907 | "required": [
908 | "name",
909 | "type"
910 | ],
911 | "properties": {
912 | "name": {
913 | "title": "Name",
914 | "description": "A name for this field.",
915 | "type": "string"
916 | },
917 | "title": {
918 | "title": "Title",
919 | "description": "A human-readable title.",
920 | "type": "string",
921 | "examples": [
922 | "{\n \"title\": \"My Package Title\"\n}\n"
923 | ]
924 | },
925 | "description": {
926 | "title": "Description",
927 | "description": "A text description. Markdown is encouraged.",
928 | "type": "string",
929 | "examples": [
930 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
931 | ]
932 | },
933 | "type": {
934 | "description": "The type keyword, which `MUST` be a value of `object`.",
935 | "enum": [
936 | "object"
937 | ]
938 | },
939 | "format": {
940 | "description": "There are no format keyword options for `object`: only `default` is allowed.",
941 | "enum": [
942 | "default"
943 | ],
944 | "default": "default"
945 | },
946 | "constraints": {
947 | "title": "Constraints",
948 | "description": "The following constraints apply for `object` fields.",
949 | "type": "object",
950 | "properties": {
951 | "required": {
952 | "type": "boolean",
953 | "description": "Indicates whether a property must have a value for each instance.",
954 | "context": "An empty string is considered to be a missing value."
955 | },
956 | "unique": {
957 | "type": "boolean",
958 | "description": "When `true`, each value for the property `MUST` be unique."
959 | },
960 | "enum": {
961 | "oneOf": [
962 | {
963 | "type": "array",
964 | "minItems": 1,
965 | "uniqueItems": true,
966 | "items": {
967 | "type": "string"
968 | }
969 | },
970 | {
971 | "type": "array",
972 | "minItems": 1,
973 | "uniqueItems": true,
974 | "items": {
975 | "type": "object"
976 | }
977 | }
978 | ]
979 | },
980 | "minLength": {
981 | "type": "integer",
982 | "description": "An integer that specifies the minimum length of a value."
983 | },
984 | "maxLength": {
985 | "type": "integer",
986 | "description": "An integer that specifies the maximum length of a value."
987 | }
988 | }
989 | },
990 | "rdfType": {
991 | "type": "string",
992 | "description": "The RDF type for this field."
993 | }
994 | },
995 | "examples": [
996 | "{\n \"name\": \"extra\"\n \"type\": \"object\"\n}\n"
997 | ]
998 | },
999 | {
1000 | "type": "object",
1001 | "title": "GeoPoint Field",
1002 | "description": "The field contains data describing a geographic point.",
1003 | "required": [
1004 | "name",
1005 | "type"
1006 | ],
1007 | "properties": {
1008 | "name": {
1009 | "title": "Name",
1010 | "description": "A name for this field.",
1011 | "type": "string"
1012 | },
1013 | "title": {
1014 | "title": "Title",
1015 | "description": "A human-readable title.",
1016 | "type": "string",
1017 | "examples": [
1018 | "{\n \"title\": \"My Package Title\"\n}\n"
1019 | ]
1020 | },
1021 | "description": {
1022 | "title": "Description",
1023 | "description": "A text description. Markdown is encouraged.",
1024 | "type": "string",
1025 | "examples": [
1026 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
1027 | ]
1028 | },
1029 | "type": {
1030 | "description": "The type keyword, which `MUST` be a value of `geopoint`.",
1031 | "enum": [
1032 | "geopoint"
1033 | ]
1034 | },
1035 | "format": {
1036 | "description": "The format keyword options for `geopoint` are `default`,`array`, and `object`.",
1037 | "context": "The following `format` options are supported:\n * **default**: A string of the pattern 'lon, lat', where `lon` is the longitude and `lat` is the latitude.\n * **array**: An array of exactly two items, where each item is either a number, or a string parsable as a number, and the first item is `lon` and the second item is `lat`.\n * **object**: A JSON object with exactly two keys, `lat` and `lon`",
1038 | "notes": [
1039 | "Implementations `MUST` strip all white space in the default format of `lon, lat`."
1040 | ],
1041 | "enum": [
1042 | "default",
1043 | "array",
1044 | "object"
1045 | ],
1046 | "default": "default"
1047 | },
1048 | "constraints": {
1049 | "title": "Constraints",
1050 | "description": "The following constraints are supported for `geopoint` fields.",
1051 | "type": "object",
1052 | "properties": {
1053 | "required": {
1054 | "type": "boolean",
1055 | "description": "Indicates whether a property must have a value for each instance.",
1056 | "context": "An empty string is considered to be a missing value."
1057 | },
1058 | "unique": {
1059 | "type": "boolean",
1060 | "description": "When `true`, each value for the property `MUST` be unique."
1061 | },
1062 | "enum": {
1063 | "oneOf": [
1064 | {
1065 | "type": "array",
1066 | "minItems": 1,
1067 | "uniqueItems": true,
1068 | "items": {
1069 | "type": "string"
1070 | }
1071 | },
1072 | {
1073 | "type": "array",
1074 | "minItems": 1,
1075 | "uniqueItems": true,
1076 | "items": {
1077 | "type": "array"
1078 | }
1079 | },
1080 | {
1081 | "type": "array",
1082 | "minItems": 1,
1083 | "uniqueItems": true,
1084 | "items": {
1085 | "type": "object"
1086 | }
1087 | }
1088 | ]
1089 | }
1090 | }
1091 | },
1092 | "rdfType": {
1093 | "type": "string",
1094 | "description": "The RDF type for this field."
1095 | }
1096 | },
1097 | "examples": [
1098 | "{\n \"name\": \"post_office\",\n \"type\": \"geopoint\"\n}\n",
1099 | "{\n \"name\": \"post_office\",\n \"type\": \"geopoint\",\n \"format\": \"array\"\n}\n"
1100 | ]
1101 | },
1102 | {
1103 | "type": "object",
1104 | "title": "GeoJSON Field",
1105 | "description": "The field contains a JSON object according to GeoJSON or TopoJSON",
1106 | "required": [
1107 | "name",
1108 | "type"
1109 | ],
1110 | "properties": {
1111 | "name": {
1112 | "title": "Name",
1113 | "description": "A name for this field.",
1114 | "type": "string"
1115 | },
1116 | "title": {
1117 | "title": "Title",
1118 | "description": "A human-readable title.",
1119 | "type": "string",
1120 | "examples": [
1121 | "{\n \"title\": \"My Package Title\"\n}\n"
1122 | ]
1123 | },
1124 | "description": {
1125 | "title": "Description",
1126 | "description": "A text description. Markdown is encouraged.",
1127 | "type": "string",
1128 | "examples": [
1129 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
1130 | ]
1131 | },
1132 | "type": {
1133 | "description": "The type keyword, which `MUST` be a value of `geojson`.",
1134 | "enum": [
1135 | "geojson"
1136 | ]
1137 | },
1138 | "format": {
1139 | "description": "The format keyword options for `geojson` are `default` and `topojson`.",
1140 | "context": "The following `format` options are supported:\n * **default**: A geojson object as per the [GeoJSON spec](http://geojson.org/).\n * **topojson**: A topojson object as per the [TopoJSON spec](https://github.com/topojson/topojson-specification/blob/master/README.md)",
1141 | "enum": [
1142 | "default",
1143 | "topojson"
1144 | ],
1145 | "default": "default"
1146 | },
1147 | "constraints": {
1148 | "title": "Constraints",
1149 | "description": "The following constraints are supported for `geojson` fields.",
1150 | "type": "object",
1151 | "properties": {
1152 | "required": {
1153 | "type": "boolean",
1154 | "description": "Indicates whether a property must have a value for each instance.",
1155 | "context": "An empty string is considered to be a missing value."
1156 | },
1157 | "unique": {
1158 | "type": "boolean",
1159 | "description": "When `true`, each value for the property `MUST` be unique."
1160 | },
1161 | "enum": {
1162 | "oneOf": [
1163 | {
1164 | "type": "array",
1165 | "minItems": 1,
1166 | "uniqueItems": true,
1167 | "items": {
1168 | "type": "string"
1169 | }
1170 | },
1171 | {
1172 | "type": "array",
1173 | "minItems": 1,
1174 | "uniqueItems": true,
1175 | "items": {
1176 | "type": "object"
1177 | }
1178 | }
1179 | ]
1180 | },
1181 | "minLength": {
1182 | "type": "integer",
1183 | "description": "An integer that specifies the minimum length of a value."
1184 | },
1185 | "maxLength": {
1186 | "type": "integer",
1187 | "description": "An integer that specifies the maximum length of a value."
1188 | }
1189 | }
1190 | },
1191 | "rdfType": {
1192 | "type": "string",
1193 | "description": "The RDF type for this field."
1194 | }
1195 | },
1196 | "examples": [
1197 | "{\n \"name\": \"city_limits\",\n \"type\": \"geojson\"\n}\n",
1198 | "{\n \"name\": \"city_limits\",\n \"type\": \"geojson\",\n \"format\": \"topojson\"\n}\n"
1199 | ]
1200 | },
1201 | {
1202 | "type": "object",
1203 | "title": "Array Field",
1204 | "description": "The field contains data which can be parsed as a valid JSON array.",
1205 | "required": [
1206 | "name",
1207 | "type"
1208 | ],
1209 | "properties": {
1210 | "name": {
1211 | "title": "Name",
1212 | "description": "A name for this field.",
1213 | "type": "string"
1214 | },
1215 | "title": {
1216 | "title": "Title",
1217 | "description": "A human-readable title.",
1218 | "type": "string",
1219 | "examples": [
1220 | "{\n \"title\": \"My Package Title\"\n}\n"
1221 | ]
1222 | },
1223 | "description": {
1224 | "title": "Description",
1225 | "description": "A text description. Markdown is encouraged.",
1226 | "type": "string",
1227 | "examples": [
1228 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
1229 | ]
1230 | },
1231 | "type": {
1232 | "description": "The type keyword, which `MUST` be a value of `array`.",
1233 | "enum": [
1234 | "array"
1235 | ]
1236 | },
1237 | "format": {
1238 | "description": "There are no format keyword options for `array`: only `default` is allowed.",
1239 | "enum": [
1240 | "default"
1241 | ],
1242 | "default": "default"
1243 | },
1244 | "constraints": {
1245 | "title": "Constraints",
1246 | "description": "The following constraints apply for `array` fields.",
1247 | "type": "object",
1248 | "properties": {
1249 | "required": {
1250 | "type": "boolean",
1251 | "description": "Indicates whether a property must have a value for each instance.",
1252 | "context": "An empty string is considered to be a missing value."
1253 | },
1254 | "unique": {
1255 | "type": "boolean",
1256 | "description": "When `true`, each value for the property `MUST` be unique."
1257 | },
1258 | "enum": {
1259 | "oneOf": [
1260 | {
1261 | "type": "array",
1262 | "minItems": 1,
1263 | "uniqueItems": true,
1264 | "items": {
1265 | "type": "string"
1266 | }
1267 | },
1268 | {
1269 | "type": "array",
1270 | "minItems": 1,
1271 | "uniqueItems": true,
1272 | "items": {
1273 | "type": "array"
1274 | }
1275 | }
1276 | ]
1277 | },
1278 | "minLength": {
1279 | "type": "integer",
1280 | "description": "An integer that specifies the minimum length of a value."
1281 | },
1282 | "maxLength": {
1283 | "type": "integer",
1284 | "description": "An integer that specifies the maximum length of a value."
1285 | }
1286 | }
1287 | },
1288 | "rdfType": {
1289 | "type": "string",
1290 | "description": "The RDF type for this field."
1291 | }
1292 | },
1293 | "examples": [
1294 | "{\n \"name\": \"options\"\n \"type\": \"array\"\n}\n"
1295 | ]
1296 | },
1297 | {
1298 | "type": "object",
1299 | "title": "Duration Field",
1300 | "description": "The field contains a duration of time.",
1301 | "context": "The lexical representation for duration is the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) extended format `PnYnMnDTnHnMnS`, where `nY` represents the number of years, `nM` the number of months, `nD` the number of days, 'T' is the date/time separator, `nH` the number of hours, `nM` the number of minutes and `nS` the number of seconds. The number of seconds can include decimal digits to arbitrary precision. Date and time elements including their designator may be omitted if their value is zero, and lower order elements may also be omitted for reduced precision. Here we follow the definition of [XML Schema duration datatype](http://www.w3.org/TR/xmlschema-2/#duration) directly and that definition is implicitly inlined here.",
1302 | "required": [
1303 | "name",
1304 | "type"
1305 | ],
1306 | "properties": {
1307 | "name": {
1308 | "title": "Name",
1309 | "description": "A name for this field.",
1310 | "type": "string"
1311 | },
1312 | "title": {
1313 | "title": "Title",
1314 | "description": "A human-readable title.",
1315 | "type": "string",
1316 | "examples": [
1317 | "{\n \"title\": \"My Package Title\"\n}\n"
1318 | ]
1319 | },
1320 | "description": {
1321 | "title": "Description",
1322 | "description": "A text description. Markdown is encouraged.",
1323 | "type": "string",
1324 | "examples": [
1325 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
1326 | ]
1327 | },
1328 | "type": {
1329 | "description": "The type keyword, which `MUST` be a value of `duration`.",
1330 | "enum": [
1331 | "duration"
1332 | ]
1333 | },
1334 | "format": {
1335 | "description": "There are no format keyword options for `duration`: only `default` is allowed.",
1336 | "enum": [
1337 | "default"
1338 | ],
1339 | "default": "default"
1340 | },
1341 | "constraints": {
1342 | "title": "Constraints",
1343 | "description": "The following constraints are supported for `duration` fields.",
1344 | "type": "object",
1345 | "properties": {
1346 | "required": {
1347 | "type": "boolean",
1348 | "description": "Indicates whether a property must have a value for each instance.",
1349 | "context": "An empty string is considered to be a missing value."
1350 | },
1351 | "unique": {
1352 | "type": "boolean",
1353 | "description": "When `true`, each value for the property `MUST` be unique."
1354 | },
1355 | "enum": {
1356 | "type": "array",
1357 | "minItems": 1,
1358 | "uniqueItems": true,
1359 | "items": {
1360 | "type": "string"
1361 | }
1362 | },
1363 | "minimum": {
1364 | "type": "string"
1365 | },
1366 | "maximum": {
1367 | "type": "string"
1368 | }
1369 | }
1370 | },
1371 | "rdfType": {
1372 | "type": "string",
1373 | "description": "The RDF type for this field."
1374 | }
1375 | },
1376 | "examples": [
1377 | "{\n \"name\": \"period\"\n \"type\": \"duration\"\n}\n"
1378 | ]
1379 | },
1380 | {
1381 | "type": "object",
1382 | "title": "Any Field",
1383 | "description": "Any value is accepted, including values that are not captured by the type/format/constraint requirements of the specification.",
1384 | "required": [
1385 | "name",
1386 | "type"
1387 | ],
1388 | "properties": {
1389 | "name": {
1390 | "title": "Name",
1391 | "description": "A name for this field.",
1392 | "type": "string"
1393 | },
1394 | "title": {
1395 | "title": "Title",
1396 | "description": "A human-readable title.",
1397 | "type": "string",
1398 | "examples": [
1399 | "{\n \"title\": \"My Package Title\"\n}\n"
1400 | ]
1401 | },
1402 | "description": {
1403 | "title": "Description",
1404 | "description": "A text description. Markdown is encouraged.",
1405 | "type": "string",
1406 | "examples": [
1407 | "{\n \"description\": \"# My Package description\\nAll about my package.\"\n}\n"
1408 | ]
1409 | },
1410 | "type": {
1411 | "description": "The type keyword, which `MUST` be a value of `any`.",
1412 | "enum": [
1413 | "any"
1414 | ]
1415 | },
1416 | "constraints": {
1417 | "title": "Constraints",
1418 | "description": "The following constraints apply to `any` fields.",
1419 | "type": "object",
1420 | "properties": {
1421 | "required": {
1422 | "type": "boolean",
1423 | "description": "Indicates whether a property must have a value for each instance.",
1424 | "context": "An empty string is considered to be a missing value."
1425 | },
1426 | "unique": {
1427 | "type": "boolean",
1428 | "description": "When `true`, each value for the property `MUST` be unique."
1429 | },
1430 | "enum": {
1431 | "type": "array",
1432 | "minItems": 1,
1433 | "uniqueItems": true
1434 | }
1435 | }
1436 | },
1437 | "rdfType": {
1438 | "type": "string",
1439 | "description": "The RDF type for this field."
1440 | }
1441 | },
1442 | "examples": [
1443 | "{\n \"name\": \"notes\",\n \"type\": \"any\"\n"
1444 | ]
1445 | }
1446 | ]
1447 | },
1448 | "description": "An `array` of Table Schema Field objects.",
1449 | "examples": [
1450 | "{\n \"fields\": [\n {\n \"name\": \"my-field-name\"\n }\n ]\n}\n",
1451 | "{\n \"fields\": [\n {\n \"name\": \"my-field-name\",\n \"type\": \"number\"\n },\n {\n \"name\": \"my-field-name-2\",\n \"type\": \"string\",\n \"format\": \"email\"\n }\n ]\n}\n"
1452 | ]
1453 | },
1454 | "primaryKey": {
1455 | "oneOf": [
1456 | {
1457 | "type": "array",
1458 | "minItems": 1,
1459 | "uniqueItems": true,
1460 | "items": {
1461 | "type": "string"
1462 | }
1463 | },
1464 | {
1465 | "type": "string"
1466 | }
1467 | ],
1468 | "description": "A primary key is a field name or an array of field names, whose values `MUST` uniquely identify each row in the table.",
1469 | "context": "Field name in the `primaryKey` `MUST` be unique, and `MUST` match a field name in the associated table. It is acceptable to have an array with a single value, indicating that the value of a single field is the primary key.",
1470 | "examples": [
1471 | "{\n \"primaryKey\": [\n \"name\"\n ]\n}\n",
1472 | "{\n \"primaryKey\": [\n \"first_name\",\n \"last_name\"\n ]\n}\n"
1473 | ]
1474 | },
1475 | "foreignKeys": {
1476 | "type": "array",
1477 | "minItems": 1,
1478 | "items": {
1479 | "title": "Table Schema Foreign Key",
1480 | "description": "Table Schema Foreign Key",
1481 | "type": "object",
1482 | "required": [
1483 | "fields",
1484 | "reference"
1485 | ],
1486 | "oneOf": [
1487 | {
1488 | "properties": {
1489 | "fields": {
1490 | "type": "array",
1491 | "items": {
1492 | "type": "string",
1493 | "minItems": 1,
1494 | "uniqueItems": true,
1495 | "description": "Fields that make up the primary key."
1496 | }
1497 | },
1498 | "reference": {
1499 | "type": "object",
1500 | "required": [
1501 | "resource",
1502 | "fields"
1503 | ],
1504 | "properties": {
1505 | "resource": {
1506 | "type": "string",
1507 | "default": ""
1508 | },
1509 | "fields": {
1510 | "type": "array",
1511 | "items": {
1512 | "type": "string"
1513 | },
1514 | "minItems": 1,
1515 | "uniqueItems": true
1516 | }
1517 | }
1518 | }
1519 | }
1520 | },
1521 | {
1522 | "properties": {
1523 | "fields": {
1524 | "type": "string",
1525 | "description": "Fields that make up the primary key."
1526 | },
1527 | "reference": {
1528 | "type": "object",
1529 | "required": [
1530 | "resource",
1531 | "fields"
1532 | ],
1533 | "properties": {
1534 | "resource": {
1535 | "type": "string",
1536 | "default": ""
1537 | },
1538 | "fields": {
1539 | "type": "string"
1540 | }
1541 | }
1542 | }
1543 | }
1544 | }
1545 | ]
1546 | },
1547 | "examples": [
1548 | "{\n \"foreignKeys\": [\n {\n \"fields\": \"state\",\n \"reference\": {\n \"resource\": \"the-resource\",\n \"fields\": \"state_id\"\n }\n }\n ]\n}\n",
1549 | "{\n \"foreignKeys\": [\n {\n \"fields\": \"state\",\n \"reference\": {\n \"resource\": \"\",\n \"fields\": \"id\"\n }\n }\n ]\n}\n"
1550 | ]
1551 | },
1552 | "missingValues": {
1553 | "type": "array",
1554 | "items": {
1555 | "type": "string"
1556 | },
1557 | "default": [
1558 | ""
1559 | ],
1560 | "description": "Values that when encountered in the source, should be considered as `null`, 'not present', or 'blank' values.",
1561 | "context": "Many datasets arrive with missing data values, either because a value was not collected or it never existed.\nMissing values may be indicated simply by the value being empty in other cases a special value may have been used e.g. `-`, `NaN`, `0`, `-9999` etc.\nThe `missingValues` property provides a way to indicate that these values should be interpreted as equivalent to null.\n\n`missingValues` are strings rather than being the data type of the particular field. This allows for comparison prior to casting and for fields to have missing value which are not of their type, for example a `number` field to have missing values indicated by `-`.\n\nThe default value of `missingValue` for a non-string type field is the empty string `''`. For string type fields there is no default for `missingValue` (for string fields the empty string `''` is a valid value and need not indicate null).",
1562 | "examples": [
1563 | "{\n \"missingValues\": [\n \"-\",\n \"NaN\",\n \"\"\n ]\n}\n",
1564 | "{\n \"missingValues\": []\n}\n"
1565 | ]
1566 | }
1567 | },
1568 | "examples": [
1569 | "{\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"
1570 | ]
1571 | }
1572 | '''
1573 |
--------------------------------------------------------------------------------
/tableschema_to_template/validation_factory.py:
--------------------------------------------------------------------------------
1 | def get_validation(field, workbook):
2 | if 'constraints' in field and 'enum' in field['constraints']:
3 | return EnumValidation(field, workbook)
4 | if 'type' in field and field['type'] == 'number':
5 | return FloatValidation(field, workbook)
6 | if 'type' in field and field['type'] == 'integer':
7 | return IntegerValidation(field, workbook)
8 | if 'type' in field and field['type'] == 'boolean':
9 | return BooleanValidation(field, workbook)
10 | return BaseValidation(field, workbook)
11 |
12 |
13 | class BaseValidation():
14 | def __init__(self, field, workbook):
15 | self.field = field
16 | self.workbook = workbook
17 |
18 | def get_data_validation(self):
19 | return {
20 | 'validate': 'any'
21 | }
22 |
23 |
24 | def _get_sheet_name(field_name):
25 | '''
26 | >>> _get_sheet_name('shorter than 31')
27 | 'shorter than 31 list'
28 | >>> _get_sheet_name('longer than thirty-one characters')
29 | 'longer than th...haracters list'
30 | '''
31 | sheet_name = f"{field_name} list"
32 | if len(sheet_name) <= 31:
33 | return sheet_name
34 | return f"{sheet_name[:14]}...{sheet_name[-14:]}"
35 |
36 |
37 | def _get_enum_error_message(enum, sheet_name):
38 | '''
39 | >>> _get_enum_error_message(['A', 'B', 'C'], 'fake list')
40 | 'Value must be one of: A / B / C.'
41 | >>> _get_enum_error_message(['A', 'B', 'C', 'D', 'E', 'F'], 'fake list')
42 | 'Value must come from fake list.'
43 | '''
44 | if len(enum) < 6:
45 | return f"Value must be one of: {' / '.join(enum)}."
46 | return f"Value must come from {sheet_name}."
47 |
48 |
49 | class EnumValidation(BaseValidation):
50 | def get_data_validation(self):
51 | sheet_name = _get_sheet_name(self.field['name'])
52 | enum_sheet = self.workbook.add_worksheet(sheet_name)
53 |
54 | enum = self.field['constraints']['enum']
55 | for i, value in enumerate(enum):
56 | enum_sheet.write(i, 0, value)
57 |
58 | return {
59 | 'validate': 'list',
60 | 'source': f"='{sheet_name}'!$A$1:$A${len(enum)}",
61 | # NOTE: OpenOffice uses "." instead of "!".
62 | 'error_title': 'Value must come from list',
63 | 'error_message': _get_enum_error_message(enum, sheet_name)
64 | }
65 |
66 |
67 | class NumberValidation(BaseValidation):
68 | def get_bound(self, bound_name, default):
69 | if 'constraints' in self.field and bound_name in self.field['constraints']:
70 | return self.field['constraints'][bound_name]
71 | return default
72 |
73 | def get_bound_message(self):
74 | if 'constraints' not in self.field:
75 | return ''
76 | cons = self.field['constraints']
77 | if 'minimum' in cons and 'maximum' in cons:
78 | return f" between {cons['minimum']} and {cons['maximum']}"
79 | if 'minimum' in cons:
80 | return f" >= {cons['minimum']}"
81 | if 'maximum' in cons:
82 | return f" <= {cons['maximum']}"
83 | return ''
84 |
85 |
86 | class FloatValidation(NumberValidation):
87 | def get_data_validation(self):
88 | return {
89 | 'validate': 'decimal',
90 | 'error_title': 'Not a number',
91 | 'error_message':
92 | f"The values in this column must be numbers{self.get_bound_message()}.",
93 | 'criteria': 'between',
94 | 'minimum': self.get_min(),
95 | 'maximum': self.get_max()
96 | }
97 |
98 | def get_min(self):
99 | # https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
100 | return self.get_bound('minimum', -1e+307)
101 |
102 | def get_max(self):
103 | return self.get_bound('maximum', 1e+307)
104 |
105 |
106 | class IntegerValidation(NumberValidation):
107 | def get_data_validation(self):
108 | return {
109 | 'validate': 'integer',
110 | 'error_title': 'Not an integer',
111 | 'error_message':
112 | f"The values in this column must be integers{self.get_bound_message()}.",
113 | 'criteria': 'between',
114 | 'minimum': self.get_min(),
115 | 'maximum': self.get_max()
116 | }
117 |
118 | def get_min(self):
119 | return self.get_bound('minimum', -2147483647)
120 |
121 | def get_max(self):
122 | return self.get_bound('maximum', 2147483647)
123 |
124 |
125 | class BooleanValidation(BaseValidation):
126 | def get_data_validation(self):
127 | return {
128 | 'validate': 'list',
129 | 'source': ['TRUE', 'FALSE'],
130 | 'error_title': 'Not a boolean',
131 | 'error_message': 'The values in this column must be "TRUE" or "FALSE".'
132 | }
133 |
--------------------------------------------------------------------------------
/test-cli.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -o errexit
3 | set -o pipefail
4 |
5 | red=`tput setaf 1`
6 | green=`tput setaf 2`
7 | reset=`tput sgr0`
8 |
9 | die() { set +v; echo "${red}$*${reset}" 1>&2 ; sleep 1; exit 1; }
10 |
11 | function test_good() {
12 | # Make tempdir and cleanup afterwards.
13 | OLD_DIR=`mktemp -d`
14 | NEW_DIR=`mktemp -d`
15 | NEW_XLSX="$NEW_DIR/template.xlsx"
16 |
17 | PYTHONPATH="${PYTHONPATH}:tableschema_to_template" \
18 | tableschema_to_template/ts2xl.py \
19 | tests/fixtures/schema.yaml $NEW_XLSX \
20 | --sheet_name 'Enter data here' \
21 | --idempotent
22 | unzip -q $NEW_XLSX -d $NEW_DIR
23 |
24 | cp tests/fixtures/template.xlsx $OLD_DIR
25 | unzip -q $OLD_DIR/template.xlsx -d $OLD_DIR
26 |
27 | for UNZIPPED_PATH in `cd tests/fixtures/output-unzipped; find . | grep .xml`; do
28 | # We look in output-unzipped only to get a list of files:
29 | # Those have been pretty-printed, and should not be directly compared against the command-line output.
30 | # Here, we just unzip and do a byte-wise comparison of the XML.
31 | cmp -s $NEW_DIR/$UNZIPPED_PATH \
32 | $OLD_DIR/$UNZIPPED_PATH \
33 | || die "On $UNZIPPED_PATH, CLI output ($NEW_DIR) output does not match fixture ($OLD_DIR). Consider:
34 | cp $NEW_XLSX ./tests/fixtures/"
35 | echo "Newly generated XSLX matches XLSX fixture on $UNZIPPED_PATH"
36 | done
37 | rm -rf $NEW_DIR
38 | rm -rf $OLD_DIR
39 | }
40 |
41 | function test_bad() {
42 | ( ! PYTHONPATH="${PYTHONPATH}:tableschema_to_template" \
43 | tableschema_to_template/ts2xl.py <(echo '{}') /tmp/should-not-exist.xlsx \
44 | 2>&1 ) \
45 | | grep "Not a valid Table Schema: 'fields' is \(a \)\?required property" \
46 | || die 'Did not see expected error'
47 | # The error message changed slightly between versions.
48 | }
49 |
50 | function test_cli_doc() {
51 | diff \
52 | <(perl -ne 'print if /usage:/../```/ and ! /```/' README-cli.md) \
53 | <(PYTHONPATH="${PYTHONPATH}:tableschema_to_template" tableschema_to_template/ts2xl.py --help) \
54 | || die 'Update README-cli.md'
55 | }
56 |
57 | function test_py_doc() {
58 | # Plain 'pydoc' ran wrong version on Travis.
59 | diff --ignore-all-space \
60 | <(grep -v '```' README-py.md) \
61 | <(python -m pydoc tableschema_to_template.create_xlsx) \
62 | || die 'Update README-py.md'
63 | }
64 |
65 | for TEST in `declare -F | grep test | sed -e 's/declare -f //'`; do
66 | echo "${green}${TEST}${reset}"
67 | $TEST
68 | done
69 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -o errexit
3 | set -o pipefail
4 |
5 | red=`tput setaf 1`
6 | green=`tput setaf 2`
7 | reset=`tput sgr0`
8 |
9 | start() { [[ -z $CI ]] || echo travis_fold':'start:$1; echo ${green}$1${reset}; }
10 | end() { [[ -z $CI ]] || echo travis_fold':'end:$1; }
11 | die() { set +v; echo "${red}$*${reset}" 1>&2 ; sleep 1; exit 1; }
12 |
13 | start flake8
14 | flake8 || die "Try: autopep8 --in-place --aggressive -r ."
15 | end flake8
16 |
17 | start pytest
18 | PYTHONPATH="${PYTHONPATH}:tableschema_to_template" pytest -vv --assert=plain --doctest-modules
19 | # "plain" means that instead of a diff, we see the full, untruncated assertion message.
20 | end pytest
21 |
22 | start cli
23 | ./test-cli.sh || die 'test-cli.sh failed'
24 | end cli
25 |
26 | start changelog
27 | if [ "$TRAVIS_BRANCH" != 'main' ]; then
28 | diff CHANGELOG.md <(curl -s https://raw.githubusercontent.com/hubmapconsortium/tableschema-to-template/main/CHANGELOG.md) \
29 | && die 'Update CHANGELOG.md'
30 | fi
31 | end changelog
--------------------------------------------------------------------------------
/tests/fixtures/README.md:
--------------------------------------------------------------------------------
1 | The files in `output-unzipped` have been pretty-printed, to make comparisons easier,
2 | but this means they do not match the contents of the Excel file on a character-by-character basis.
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/[Content_Types].xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/_rels/.rels:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/docProps/app.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Microsoft Excel
4 | 0
5 | false
6 |
7 |
8 |
9 | Worksheets
10 |
11 |
12 | 3
13 |
14 |
15 |
16 |
17 |
18 | Export this as TSV
19 | abc list
20 | xyz list
21 |
22 |
23 |
24 | false
25 | false
26 | false
27 | 12.0000
28 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/docProps/core.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 2000-01-01T00:00:00Z
6 | 2000-01-01T00:00:00Z
7 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/_rels/workbook.xml.rels:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/comments1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Start of the alphabet
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | End of the alphabet
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Any string
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Any number >= zero
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | An integer between 1 and 10
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | Any boolean
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/drawings/vmlDrawing1.vml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 1, 15, 0, 2, 3, 15, 3, 16
20 | False
21 | 0
22 | 0
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | 2, 15, 0, 2, 4, 15, 3, 16
36 | False
37 | 0
38 | 1
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 3, 15, 0, 2, 5, 15, 3, 16
52 | False
53 | 0
54 | 2
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 4, 15, 0, 2, 6, 15, 3, 16
68 | False
69 | 0
70 | 3
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 5, 15, 0, 2, 7, 15, 3, 16
84 | False
85 | 0
86 | 4
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 6, 15, 0, 2, 8, 15, 3, 16
100 | False
101 | 0
102 | 5
103 |
104 |
105 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/sharedStrings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | abc
5 |
6 |
7 | A
8 |
9 |
10 | B
11 |
12 |
13 | C
14 |
15 |
16 | xyz
17 |
18 |
19 | X
20 |
21 |
22 | Y
23 |
24 |
25 | Z
26 |
27 |
28 | string
29 |
30 |
31 | number
32 |
33 |
34 | integer
35 |
36 |
37 | boolean
38 |
39 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/theme/theme1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/workbook.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/worksheets/_rels/sheet1.xml.rels:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/worksheets/sheet1.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 0
15 |
16 |
17 | 4
18 |
19 |
20 | 8
21 |
22 |
23 | 9
24 |
25 |
26 | 10
27 |
28 |
29 | 11
30 |
31 |
32 |
33 |
34 |
35 | 'abc list'!$A$1:$A$3
36 |
37 |
38 | 'xyz list'!$A$1:$A$3
39 |
40 |
41 | 0
42 | 1e+307
43 |
44 |
45 | 1
46 | 10
47 |
48 |
49 | "TRUE,FALSE"
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/worksheets/sheet2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 1
12 |
13 |
14 |
15 |
16 | 2
17 |
18 |
19 |
20 |
21 | 3
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/fixtures/output-unzipped/xl/worksheets/sheet3.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | 5
12 |
13 |
14 |
15 |
16 | 6
17 |
18 |
19 |
20 |
21 | 7
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/fixtures/schema.yaml:
--------------------------------------------------------------------------------
1 |
2 | fields:
3 | -
4 | name: abc
5 | description: Start of the alphabet
6 | constraints:
7 | enum:
8 | - A
9 | - B
10 | - C
11 | -
12 | name: xyz
13 | description: End of the alphabet
14 | constraints:
15 | enum:
16 | - X
17 | - Y
18 | - Z
19 | -
20 | name: string
21 | description: Any string
22 | -
23 | name: number
24 | description: Any number >= zero
25 | type: number
26 | constraints:
27 | minimum: 0
28 | -
29 | name: integer
30 | description: An integer between 1 and 10
31 | type: integer
32 | constraints:
33 | minimum: 1
34 | maximum: 10
35 | -
36 | name: boolean
37 | description: Any boolean
38 | type: boolean
--------------------------------------------------------------------------------
/tests/fixtures/template.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hubmapconsortium/tableschema-to-template/b458291fb054be80da8260805c6a6dd7d2439c6e/tests/fixtures/template.xlsx
--------------------------------------------------------------------------------
/tests/test_create_xlsx.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from zipfile import ZipFile
3 | import os
4 |
5 | from yattag import indent
6 | import pytest
7 | from yaml import safe_load
8 |
9 | from create_xlsx import create_xlsx
10 |
11 |
12 | @pytest.fixture(scope="module")
13 | def xlsx_path():
14 | schema_path = Path(__file__).parent / 'fixtures/schema.yaml'
15 | schema = safe_load(schema_path.read_text())
16 | # Use /tmp rather than TemporaryDirectory so it can be inspected if tests fail.
17 | xlsx_tmp_path = '/tmp/template.xlsx'
18 | create_xlsx(schema, xlsx_tmp_path, idempotent=True)
19 | yield xlsx_tmp_path
20 |
21 |
22 | def assert_matches_fixture(xlsx_path, zip_path):
23 | # zipfile.Path is introduced in Python3.8, and could make this cleaner:
24 | # xml_string = zipfile.Path(xlsx_path, zip_path).read_text()
25 | with ZipFile(xlsx_path) as zip_handle:
26 | with zip_handle.open(zip_path) as file_handle:
27 | xml_string = file_handle.read().decode('utf-8')
28 |
29 | # Before Python3.8, attribute order is not stable in minidom,
30 | # so we need to use an outside library.
31 | pretty_xml = indent(xml_string)
32 | pretty_xml_fixture_path = (
33 | Path(__file__).parent / 'fixtures/output-unzipped' / zip_path
34 | )
35 |
36 | pretty_xml_tmp_path = Path('/tmp') / Path(zip_path).name
37 | pretty_xml_tmp_path.write_text(pretty_xml)
38 |
39 | assert pretty_xml.strip() == \
40 | pretty_xml_fixture_path.read_text().strip(), \
41 | 'For more details:\n' + \
42 | f' diff {pretty_xml_tmp_path} {pretty_xml_fixture_path}\n' + \
43 | 'To fix: update XML fixture?\n' + \
44 | f' cp {pretty_xml_tmp_path} {pretty_xml_fixture_path}\n' + \
45 | 'Or update Excel file?\n' + \
46 | f' cp {xlsx_path} {Path(__file__).parent / "fixtures/template.xlsx"}'
47 |
48 |
49 | unzipped_dir = 'tests/fixtures/output-unzipped'
50 |
51 |
52 | @pytest.mark.parametrize(
53 | "zip_path",
54 | [str((Path(base) / file).relative_to(unzipped_dir))
55 | for (base, dirs, files) in os.walk(unzipped_dir)
56 | for file in files]
57 | )
58 | def test_create_xlsx(xlsx_path, zip_path):
59 | assert_matches_fixture(xlsx_path, zip_path)
60 |
--------------------------------------------------------------------------------