diff --git a/.coveragerc b/.coveragerc
new file mode 100755
index 0000000..c51e2ff
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,39 @@
+# .coveragerc to control coverage.py
+[run]
+branch = True
+omit =
+ ./docs/*
+ ./docker/*
+ ./examples/*
+ ./grammar/*
+ ./logs/*
+ ./tests/*
+ ./dist/*
+ ./images/*
+ ./output/*
+
+[report]
+# Regexes for lines to exclude from consideration
+exclude_lines =
+ # Have to re-enable the standard pragma
+ pragma: no cover
+
+ # Don't complain about missing debug-only code:
+ def __repr__
+ if self\.debug
+
+ # Don't complain if tests don't hit defensive assertion code:
+ raise AssertionError
+ raise NotImplementedError
+
+ # Don't complain if non-runnable code isn't run:
+ if 0:
+ if __name__ == .__main__.:
+
+ # Don't complain about abstract methods, they aren't run:
+ @(abc\.)?abstractmethod
+
+ignore_errors = True
+
+[html]
+directory = coverage_html_report
\ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
index 3cd9d5e..d530d6d 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,5 @@
.venv/
-doc/
+docs/
examples/
test/
*.jsonld
@@ -8,7 +8,6 @@ tox.ini
test-requirements.txt
README.md
.stestr.conf
-.pre-commit-config.yaml
.gitignore
logs/access.log
.git/
diff --git a/.gitignore b/.gitignore
index 795f69b..1e3ef38 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,7 +102,6 @@ celerybeat.pid
*.sage.py
# Environments
-.env
.venv
env/
venv/
@@ -130,8 +129,13 @@ dmypy.json
# IDE
.idea
+.vscode
# Temporal generated JSON-LD documents
*.jsonld
output
output/*.jsonld
+tests/test1.py
+
+# logs folder
+logs
diff --git a/agent.py b/agent.py
index a56f3fb..55a5de3 100644
--- a/agent.py
+++ b/agent.py
@@ -24,13 +24,14 @@
from sdmx2jsonld.transform.parser import Parser
from api.server import launch
from sdmx2jsonld.exceptions import UnexpectedEOF, UnexpectedInput, UnexpectedToken
+from ngsild.ngsild_connector import NGSILDConnector
-if __name__ == '__main__':
+if __name__ == "__main__":
args = parse_cli()
- if args['run'] is True:
- file_in = args['--input']
- generate_files = args['--output']
+ if args["run"] is True:
+ file_in = args["--input"]
+ generate_files = args["--output"]
my_parser = Parser()
@@ -43,10 +44,8 @@
except UnexpectedEOF as e:
print(e)
- elif args['server'] is True:
- port = int(args['--port'])
- host = args['--host']
+ elif args["server"] is True:
+ port = int(args["--port"])
+ host = args["--host"]
- launch(app="api.server:application",
- host=host,
- port=port)
+ launch(app="api.server:application", host=host, port=port)
diff --git a/api/custom_logging.py b/api/custom_logging.py
index 4465e1f..26c9712 100644
--- a/api/custom_logging.py
+++ b/api/custom_logging.py
@@ -30,12 +30,12 @@
class InterceptHandler(Handler):
loglevel_mapping = {
- 50: 'CRITICAL',
- 40: 'ERROR',
- 30: 'WARNING',
- 20: 'INFO',
- 10: 'DEBUG',
- 0: 'NOTSET',
+ 50: "CRITICAL",
+ 40: "ERROR",
+ 30: "WARNING",
+ 20: "INFO",
+ 10: "DEBUG",
+ 0: "NOTSET",
}
def emit(self, record):
@@ -49,46 +49,32 @@ def emit(self, record):
frame = frame.f_back
depth += 1
- log = logger.bind(request_id='app')
- log.opt(
- depth=depth,
- exception=record.exc_info
- ).log(level,record.getMessage())
+ log = logger.bind(request_id="app")
+ log.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
class CustomizeLogger:
-
@classmethod
def make_logger(cls, config_path: Path):
-
config = cls.load_logging_config(config_path)
- logging_config = config.get('logger')
+ logging_config = config.get("logger")
logger = cls.customize_logging(
- filepath=logging_config.get('path'),
- level=logging_config.get('level'),
- retention=logging_config.get('retention'),
- rotation=logging_config.get('rotation'),
- format=logging_config.get('format'))
+ filepath=logging_config.get("path"),
+ level=logging_config.get("level"),
+ retention=logging_config.get("retention"),
+ rotation=logging_config.get("rotation"),
+ format=logging_config.get("format"),
+ )
return logger
@classmethod
- def customize_logging(cls,
- filepath: Path,
- level: str,
- rotation: str,
- retention: str,
- format: str):
+ def customize_logging(cls, filepath: Path, level: str, rotation: str, retention: str, format: str):
logger.remove()
- logger.add(
- sys.stdout,
- enqueue=True,
- backtrace=True,
- level=level.upper(),
- format=format)
+ logger.add(sys.stdout, enqueue=True, backtrace=True, level=level.upper(), format=format)
logger.add(
str(filepath),
@@ -97,12 +83,13 @@ def customize_logging(cls,
enqueue=True,
backtrace=True,
level=level.upper(),
- format=format)
+ format=format,
+ )
basicConfig(handlers=[InterceptHandler()], level=0)
getLogger("uvicorn.access").handlers = [InterceptHandler()]
- for _log in ['uvicorn', 'uvicorn.error', 'uvicorn.access', 'fastapi']:
+ for _log in ["uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"]:
_logger = getLogger(_log)
_logger.handlers = [InterceptHandler()]
@@ -113,4 +100,4 @@ def load_logging_config(cls, config_path):
config = None
with open(config_path) as config_file:
config = load(config_file)
- return config
\ No newline at end of file
+ return config
diff --git a/api/server.py b/api/server.py
index 62d468c..a3c339d 100644
--- a/api/server.py
+++ b/api/server.py
@@ -21,13 +21,21 @@
##
from fastapi import FastAPI, UploadFile, Request, Response, status, HTTPException
+from fastapi.logger import logger as fastapi_logger
from uvicorn import run
from os.path import splitext
from sdmx2jsonld.transform.parser import Parser
from datetime import datetime
from cli.command import __version__
-from secure import Server, ContentSecurityPolicy, StrictTransportSecurity, \
- ReferrerPolicy, PermissionsPolicy, CacheControl, Secure
+from secure import (
+ Server,
+ ContentSecurityPolicy,
+ StrictTransportSecurity,
+ ReferrerPolicy,
+ PermissionsPolicy,
+ CacheControl,
+ Secure,
+)
from logging import getLogger
from pathlib import Path
from api.custom_logging import CustomizeLogger
@@ -35,16 +43,18 @@
from json import load, loads
from sdmx2jsonld.exceptions import UnexpectedEOF, UnexpectedInput, UnexpectedToken
from io import StringIO
+from ngsild.ngsild_connector import NGSILDConnector
initial_uptime = datetime.now()
logger = getLogger(__name__)
def create_app() -> FastAPI:
- app = FastAPI(title='IoTAgent-Turtle', debug=False)
- logging_config_path = Path.cwd().joinpath('common/config.json')
- logger = CustomizeLogger.make_logger(logging_config_path)
- app.logger = logger
+ app = FastAPI(title="IoTAgent-Turtle", debug=False)
+
+ logging_config_path = Path.cwd().joinpath("common/config.json")
+ customize_logger = CustomizeLogger.make_logger(logging_config_path)
+ fastapi_logger.addHandler(customize_logger)
return app
@@ -59,20 +69,18 @@ async def set_secure_headers(request, call_next):
csp = (
ContentSecurityPolicy()
- .default_src("'none'")
- .base_uri("'self'")
- .connect_src("'self'" "api.spam.com")
- .frame_src("'none'")
- .img_src("'self'", "static.spam.com")
+ .default_src("'none'")
+ .base_uri("'self'")
+ .connect_src("'self'" "api.spam.com")
+ .frame_src("'none'")
+ .img_src("'self'", "static.spam.com")
)
hsts = StrictTransportSecurity().include_subdomains().preload().max_age(2592000)
referrer = ReferrerPolicy().no_referrer()
- permissions_value = (
- PermissionsPolicy().geolocation("self", "'spam.com'").vibrate()
- )
+ permissions_value = PermissionsPolicy().geolocation("self", "'spam.com'").vibrate()
cache_value = CacheControl().must_revalidate()
@@ -98,7 +106,7 @@ def getversion(request: Request):
"git_hash": "nogitversion",
"version": __version__,
"release_date": "no released",
- "uptime": get_uptime()
+ "uptime": get_uptime(),
}
return data
@@ -109,8 +117,8 @@ async def parse(request: Request, file: UploadFile, response: Response):
request.app.logger.info(f'Request parse file "{file.filename}"')
# check if the post request has the file part
- if splitext(file.filename)[1] != '.ttl':
- resp = {'message': 'Allowed file type is only ttl'}
+ if splitext(file.filename)[1] != ".ttl": # type: ignore[type-var]
+ resp = {"message": "Allowed file type is only ttl"}
response.status_code = status.HTTP_400_BAD_REQUEST
request.app.logger.error(f'POST /parse 400 Bad Request, file: "{file.filename}"')
else:
@@ -120,16 +128,16 @@ async def parse(request: Request, file: UploadFile, response: Response):
request.app.logger.error(f'POST /parse 500 Problem reading file: "{file.filename}"')
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
else:
- request.app.logger.info('File successfully read')
+ request.app.logger.info("File successfully read")
# Prepare the content
- content = content.decode("utf-8")
+ content = content.decode("utf-8") # type: ignore[assignment]
# Start parsing the file
my_parser = Parser()
try:
- json_object = my_parser.parsing(content=StringIO(content))
+ json_object = my_parser.parsing(content=StringIO(content)) # type: ignore[arg-type]
except UnexpectedToken as e:
request.app.logger.error(e)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@@ -143,44 +151,42 @@ async def parse(request: Request, file: UploadFile, response: Response):
request.app.logger.error(f'POST /parse 500 Problem parsing file: "{file.filename}"')
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
else:
- request.app.logger.info(f'File successfully parsed')
+ request.app.logger.info(f"File successfully parsed")
# Send the data to a FIWARE Context Broker instance
headers = {
- 'Content-Type': 'application/ld+json',
- 'Accept': 'application/ld+json'
+ "Content-Type": "application/ld+json",
+ # 'Accept': 'application/ld+json'
}
url = get_url()
- resp = "..."
try:
- request.app.logger.debug(f'Sending data:\n{json_object}')
-
- r = post(url=url, headers=headers, data=json_object, timeout=5)
-
- resp = loads(r.text)
- response.status_code = r.status_code
+ request.app.logger.debug(f"Sending data:\n{json_object}")
+ cb = NGSILDConnector()
+ resp = cb.send_data_array(json_object)
+ # resp = loads(r.text)
+ # response.status_code = r.status_code
except exceptions.Timeout as err:
- request.app.logger.error('Timeout requesting FIWARE Context Broker')
+ request.app.logger.error("Timeout requesting FIWARE Context Broker")
raise HTTPException(status_code=status.HTTP_408_REQUEST_TIMEOUT, detail=str(err))
except exceptions.ConnectionError as err:
- message = f'There was a problem connecting to the FIWARE Context Broker. URL: {url}'
+ message = f"There was a problem connecting to the FIWARE Context Broker. URL: {url}"
request.app.logger.error(message)
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(err))
except exceptions.HTTPError as e:
- request.app.logger.error(f'Call to FIWARE Context Broker failed: {e}')
+ request.app.logger.error(f"Call to FIWARE Context Broker failed: {e}")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except KeyboardInterrupt:
- request.app.logger.warning('Server interrupted by user')
+ request.app.logger.warning("Server interrupted by user")
raise
- except Exception:
- message = "Unknown error sending data to the Context Broker"
- request.app.logger.error(message)
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(message))
+ except Exception as e:
+ r = getattr(e, "message", str(e))
+ request.app.logger.error(r)
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(r))
else:
- request.app.logger.info(f'Content sent to the Context Broker')
- request.app.logger.debug(f'Status Code: {response.status_code}, Response:\n{resp}')
+ request.app.logger.info(f"Content sent to the Context Broker")
+ request.app.logger.debug(f"Status Code: {response.status_code}, Response:\n{resp}")
return resp
@@ -193,14 +199,14 @@ def get_uptime():
minutes, seconds = divmod(remainder, 60)
days, hours = divmod(hours, 24)
- fmt = '{d} days, {h} hours, {m} minutes, and {s} seconds'
+ fmt = "{d} days, {h} hours, {m} minutes, and {s} seconds"
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
def get_url():
- config_path = Path.cwd().joinpath('common/config.json')
- config = dict()
+ config_path = Path.cwd().joinpath("common/config.json")
+
with open(config_path) as config_file:
config = load(config_file)
@@ -210,7 +216,14 @@ def get_url():
def launch(app: str = "server:application", host: str = "127.0.0.1", port: int = 5000):
- run(app=app, host=host, port=port, log_level="info", reload=True, server_header=False)
+ run(
+ app=app,
+ host=host,
+ port=port,
+ log_level="info",
+ reload=True,
+ server_header=False,
+ )
if __name__ == "__main__":
diff --git a/cli/command.py b/cli/command.py
index b7fd47f..a23b0df 100644
--- a/cli/command.py
+++ b/cli/command.py
@@ -47,32 +47,35 @@
from docopt import docopt
from os.path import basename
from sys import argv
-from schema import Schema, And, Or, Use, SchemaError
+from schema import Schema, And, Or, Use, SchemaError # type: ignore
-__version__ = "0.5.2"
+__version__ = "1.0.1"
__author__ = "fla"
def parse_cli() -> dict:
if len(argv) == 1:
- argv.append('-h')
+ argv.append("-h")
- version = f'IoTAgent-Turtle version {__version__}'
+ version = f"IoTAgent-Turtle version {__version__}"
args = docopt(__doc__.format(proc=basename(argv[0])), version=version)
schema = Schema(
{
- '--help': bool,
- '--input': Or(None, Use(open, error='--input FILE, FILE should be readable')),
- '--output': bool,
- '--port': Or(None, And(Use(int), lambda n: 1 < n < 65535),
- error='--port N, N should be integer 1 < N < 65535'),
- '--host': Or(None, str, error='--host HOST should be a string'),
- '--version': bool,
- 'run': bool,
- 'server': bool
+ "--help": bool,
+ "--input": Or(None, Use(open, error="--input FILE, FILE should be readable")),
+ "--output": bool,
+ "--port": Or(
+ None,
+ And(Use(int), lambda n: 1 < n < 65535),
+ error="--port N, N should be integer 1 < N < 65535",
+ ),
+ "--host": Or(None, str, error="--host HOST should be a string"),
+ "--version": bool,
+ "run": bool,
+ "server": bool,
}
)
@@ -84,5 +87,5 @@ def parse_cli() -> dict:
return args
-if __name__ == '__main__':
+if __name__ == "__main__":
print(parse_cli())
diff --git a/doc/api.yaml b/doc/api.yaml
deleted file mode 100644
index 76c7757..0000000
--- a/doc/api.yaml
+++ /dev/null
@@ -1,114 +0,0 @@
-openapi: 3.0.3
-info:
- description: 'Spec for the IoTAgent-Turtle agent'
- version: 0.0.1
- title: IoTAgentTurtle
- contact:
- email: fernando.lopez@fiware.org
-externalDocs:
- description: Implementation on github.
- url: 'https://github.com/flopezag/IoTAgent-Turtle'
-tags:
- - name: Ops
- description: Method to provide health info about the service information
- - name: Parse
- description: Perform the parse operation.
-paths:
- '/version':
- get:
- tags:
- - Ops
- description: Provide the current health status and information vertsion of IoTAgent-Turtle.
- operationId: getversion
- responses:
- '200':
- description: Service is up and running and provide description information about the server.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
-
- '/parse':
- post:
- tags:
- - Parse
- description: Parse a SDMX Turtle file into a NGSI-LD (JSON-LD) format.
- operationId: parse
- requestBody:
- required: true
- content:
- form-data:
- schema:
- $ref: '#/components/schemas/TurtleFile'
- responses:
- '201':
- description: File succesfully read, parsed, and forwarded to the FIWARE Context Broker.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
- '500':
- description: Did not receive a valid NGSI-LD Entity.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
- '400':
- description: Did not receive a valid NGSI-LD Entity.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
- '408':
- description: Did not receive a valid NGSI-LD Entity.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
- '503':
- description: Did not receive a valid NGSI-LD Entity.
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/VersionInfo'
-components:
- schemas:
- VersionInfo:
- type: object
- description: Status of the running service
- properties:
- doc:
- type: string
- description: Link to the documentation of the component.
- example: "UP"
- git_hash:
- type: string
- description: Git hash of the current executed version.
- example: "UP"
- version:
- type: string
- description: Current version of the IoTAgent-Turtle server.
- example: "UP"
- release_date:
- type: string
- description: Release date of the executed IoTAgent-Turtle server.
- example: "UP"
- uptime:
- type: string
- description: Time that the IoTAgent-Turtle server is up and running.
- example: "UP"
- required:
- - doc
- - git_hash
- - version
- - release_date
- - uptime
-
- TurtleFile:
- type: string
- format: binary
- description: |
- This is a string
- in multiple lines.
-
- And an extra one.
diff --git a/docker/.env b/docker/.env
new file mode 100755
index 0000000..09be6ce
--- /dev/null
+++ b/docker/.env
@@ -0,0 +1,13 @@
+# Project name
+COMPOSE_PROJECT_NAME=fiware
+
+# Orion variables
+ORION_LD_PORT=1026
+ORION_LD_VERSION=1.0.0
+
+# MongoDB variables
+MONGO_DB_PORT=27017
+MONGO_DB_VERSION=4.4
+
+# IoTAgent-Turtle varaibles
+IOTAGENT_PORT=5000
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 03f69d2..06947b0 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,63 +1,57 @@
-FROM python:3.9-alpine@sha256:e80214a705236091ee3821a7e512e80bd3337b50a95392a36b9a40b8fc0ea183 as build
+FROM alpine/git as git-clone
-LABEL "maintainer"="FIWARE Foundation e.V."
-LABEL "description"="An Internet of Things Agent for the RDF DataQube Turtle format. This IoT Agent is designed to be a bridge between ISOXML/ADAPT and the ETSI NGSI-LD interface of a FIWARE Context Broker."
-LABEL "name"="iotagent-turtle"
-LABEL "summary"="IoT Agent for the DataQube representation in RDF Turtle format used in statistical environments"
+ARG PROJECT=flopezag
+ARG COMPONENT=IoTAgent-Turtle
+ARG BRANCH=develop
+ARG INSTALLATION_PATH=/opt/$COMPONENT
-LABEL "org.opencontainers.image.authors"="fernando.lopez@fiware.org"
-LABEL "org.opencontainers.image.documentation"=""
-LABEL "org.opencontainers.image.vendor"="FIWARE Foundation e.V."
-LABEL "org.opencontainers.image.licenses"="Apache2.0"
-LABEL "org.opencontainers.image.title"="iotagent-turtle"
-LABEL "org.opencontainers.image.description"="An Internet of Things Agent for the RDF DataQube Turtle format. This IoT Agent is designed to be a bridge between ISOXML/ADAPT and the ETSI NGSI-LD interface of a FIWARE Context Broker."
-LABEL "org.opencontainers.image.source"="https://github.com/flopezag/IoTAgent-Turtle"
-LABEL "org.opencontainers.image.version"="0.1.0"
-LABEL "org.python.version"="python:3.9"
+RUN mkdir -p $INSTALLATION_PATH
+RUN git clone https://github.com/$PROJECT/$COMPONENT --branch $BRANCH $INSTALLATION_PATH
-RUN apk update
-# RUN apt-get install -y --no-install-recommends \
-# build-essential gcc
-WORKDIR /usr/app
-RUN python -m venv /usr/app/venv
-ENV PATH="/usr/app/venv/bin:$PATH"
+## Install PIP Requirements
+FROM python:3.11-alpine as pip-requirements
-COPY requirements.txt .
-RUN pip install --no-cache-dir --upgrade -r requirements.txt
+ARG PROJECT=flopezag
+ARG COMPONENT=IoTAgent-Turtle
+ARG INSTALLATION_PATH=/opt/$COMPONENT
-FROM python:3.9-alpine@sha256:e80214a705236091ee3821a7e512e80bd3337b50a95392a36b9a40b8fc0ea183
+RUN mkdir -p $INSTALLATION_PATH
+COPY --from=git-clone $INSTALLATION_PATH/requirements.txt /requirements.txt
+RUN pip install --root-user-action=ignore --prefix=$INSTALLATION_PATH -r /requirements.txt
-ENV PORT=${IOTA_PORT:-5000}
+
+FROM python:3.11-alpine as final
LABEL "maintainer"="FIWARE Foundation e.V."
-LABEL "description"="An Internet of Things Agent for the RDF DataQube Turtle format. This IoT Agent is designed to be a bridge between ISOXML/ADAPT and the ETSI NGSI-LD interface of a FIWARE Context Broker."
-LABEL "name"="iotagent-turtle"
-LABEL "summary"="IoT Agent for the DataQube representation in RDF Turtle format used in statistical environments"
+LABEL "description"="An Internet of Things Agent for the RDF Turtle of SDMX DataQube format. This IoT Agent is designed to be a bridge between SDMX statistical metadata representation in RDF Turtle and ETSI NGSI-LD in JSON-LD format representation to be integrated with FIWARE components."
+LABEL "name"="iotagent-turtle"
+LABEL "summary"="IoT Agent for the DataQube (SDMX) representation in RDF Turtle format used in statistical environments."
LABEL "org.opencontainers.image.authors"="fernando.lopez@fiware.org"
-LABEL "org.opencontainers.image.documentation"=""
+LABEL "org.opencontainers.image.documentation"="https://github.com/flopezag/IoTAgent-Turtle/tree/master/doc"
LABEL "org.opencontainers.image.vendor"="FIWARE Foundation e.V."
LABEL "org.opencontainers.image.licenses"="Apache2.0"
LABEL "org.opencontainers.image.title"="iotagent-turtle"
-LABEL "org.opencontainers.image.description"="An Internet of Things Agent for the RDF DataQube Turtle format. This IoT Agent is designed to be a bridge between ISOXML/ADAPT and the ETSI NGSI-LD interface of a FIWARE Context Broker."
+LABEL "org.opencontainers.image.description"="An Internet of Things Agent for the RDF Turtle of SDMX DataQube format. This IoT Agent is designed to be a bridge between SDMX statistical metadata representation in RDF Turtle and ETSI NGSI-LD in JSON-LD format representation to be integrated with FIWARE components."
LABEL "org.opencontainers.image.source"="https://github.com/flopezag/IoTAgent-Turtle"
LABEL "org.opencontainers.image.version"="0.1.0"
-LABEL "org.python.version"="python:3.9"
+LABEL "org.python.version"="python:3.11"
-RUN addgroup -g 99 python && \
- adduser -S -u 999 -g python python
+ARG PROJECT=flopezag
+ARG COMPONENT=IoTAgent-Turtle
+ARG INSTALLATION_PATH=/opt/$COMPONENT
-RUN mkdir /usr/app && chown python:python /usr/app
-WORKDIR /usr/app
+ENV PORT=${IOTA_PORT:-5000}
-COPY --chown=python:python --from=build /usr/app/venv ./venv
-COPY --chown=python:python . .
+RUN mkdir -p $INSTALLATION_PATH
-USER 999
+COPY --from=git-clone $INSTALLATION_PATH $INSTALLATION_PATH
+COPY --from=pip-requirements $INSTALLATION_PATH /usr/local
-ENV PATH="/usr/app/venv/bin:$PATH"
-CMD ["python", "agent.py", "server", "--host", "0.0.0.0", "--port", "5000"]
-HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD curl -f https://localhost:${PORT}/version
+COPY config.json $INSTALLATION_PATH/common/config.json
+WORKDIR $INSTALLATION_PATH
EXPOSE ${PORT}
+ENTRYPOINT /usr/local/bin/python agent.py server --host 0.0.0.0 --port ${PORT}
+
diff --git a/docker/Readme.md b/docker/Readme.md
new file mode 100644
index 0000000..93e4520
--- /dev/null
+++ b/docker/Readme.md
@@ -0,0 +1,86 @@
+# Docker for server
+
+## build
+The docker is easily built using the following command - By default, it builds the docker for the **develop** branch:
+
+```
+ docker build . -t iotagent-turtle
+```
+
+If we want to build other branch, we could define the branch we want to use this way:
+```
+ docker build . -t iotagent-turtle --build-arg BRANCH=integration-cb-httpserver
+```
+
+Everything will install in /proc/IotAgent-turtle inside the container. The default config file will be the following one:
+
+```
+{
+ "broker": "http://orion-ld:1026",
+ "logger": {
+ "path": "./logs/access.log",
+ "level": "debug",
+ "rotation": "20 days",
+ "retention": "1 months",
+ "format": "{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} request id: {extra[request_id]} - {name}:{function} - {message}"
+ }
+}
+```
+
+## Use
+Let's suppose the image is named **iotagent-turtle** and we want to expose port 5000 in order to work. We can also imagine that we want to remove the container when it finishes, we can do it this way:
+```
+docker run --rm -p 5000:5000 --name io iotagent-turtle
+```
+
+However, we might have Orion-ld somewhere else. Therefore, we can provide the IP address of the host "orion-ld" where our Context Broker is listening this way:
+```
+docker run --rm -p 5000:5000 --name io --add-host=orion-ld:192.168.1.206 iotagent-turtle
+```
+
+### Overriding config.json
+We could create our own config.json and the default configuration file in the docker:
+```
+docker run --rm -p 5000:5000 --name io -v our-local-config.json:/opt/IoTAgent-turtle/common/config.json iotagent-turtle
+```
+
+### As docker file
+
+We can consider writing a docker-compose.yaml file as the following one to start everything (orion-ld, the name of our orion server is named according to our config.json file -- see "broker" key):
+
+```
+version: "3.8"
+services:
+ orion-ld:
+ image: fiware/orion-ld
+ hostname: orion-ld
+ container_name: orion-ld
+ expose:
+ - 1026
+ ports:
+ - 1026:1026
+ depends_on:
+ - fiware-orion-ld-mongo-db
+ command: -dbhost fiware-orion-ld-mongo-db -logLevel DEBUG -experimental
+
+ fiware-orion-ld-mongo-db:
+ image: mongo:5.0
+ hostname: mongo-db
+ networks:
+ - default
+ command: --nojournal
+ volumes:
+ - /data/docker/orion-ld/mongodb:/data
+
+ iotagent-turtle:
+ image: iotagent-turtle:latest
+ hostname: ioagent-turtle
+ container_name: iotagent-turtle
+ expose:
+ - 5000
+ ports:
+ - 5000:5000
+ networks:
+ - default
+```
+
diff --git a/docker/config.json b/docker/config.json
index a8c047b..81bdc44 100644
--- a/docker/config.json
+++ b/docker/config.json
@@ -1,5 +1,5 @@
{
- "broker": "http://orion:1026",
+ "broker": "http://orion-ld:1026",
"logger": {
"path": "./logs/access.log",
"level": "debug",
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
new file mode 100644
index 0000000..edcc4bb
--- /dev/null
+++ b/docker/docker-compose.yaml
@@ -0,0 +1,34 @@
+version: "3.8"
+services:
+ orion-ld:
+ image: fiware/orion-ld
+ hostname: orion-ld
+ container_name: orion-ld
+ expose:
+ - 1026
+ ports:
+ - 1026:1026
+ depends_on:
+ - fiware-orion-ld-mongo-db
+ command: -dbhost fiware-orion-ld-mongo-db -logLevel DEBUG -experimental
+
+ fiware-orion-ld-mongo-db:
+ image: mongo:5.0
+ hostname: mongo-db
+ networks:
+ - default
+ command: --nojournal
+ volumes:
+ - /data/docker/orion-ld/mongodb:/data
+
+ iotagent-turtle:
+ image: iotagent-turtle:latest
+ hostname: ioagent-turtle
+ container_name: iotagent-turtle
+ expose:
+ - 5000
+ ports:
+ - 5000:5000
+ networks:
+ - default
+
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
deleted file mode 100644
index be57f38..0000000
--- a/docker/docker-compose.yml
+++ /dev/null
@@ -1,81 +0,0 @@
-# WARNING: Do not deploy this tutorial configuration directly to a production environment
-#
-# The tutorial docker-compose files have not been written for production deployment and will not
-# scale. A proper architecture has been sacrificed to keep the narrative focused on the learning
-# goals, they are just used to deploy everything onto a single Docker machine. All FIWARE components
-# are running at full debug and extra ports have been exposed to allow for direct calls to services.
-# They also contain various obvious security flaws - passwords in plain text, no load balancing,
-# no use of HTTPS and so on.
-#
-# This is all to avoid the need of multiple machines, generating certificates, encrypting secrets
-# and so on, purely so that a single docker-compose file can be read as an example to build on,
-# not use directly.
-#
-# When deploying to a production environment, please refer to the Helm Repository
-# for FIWARE Components in order to scale up to a proper architecture:
-#
-# see: https://github.com/FIWARE/helm-charts/
-#
-version: "3.5"
-services:
- iotagent:
- image: iotagent-turtle
- hostname: iotagent-turtle
- container_name: iotagent-turtle
- networks:
- - default
- volumes:
- - ./config.json:/usr/app/common/config.json:ro
- ports:
- - "${IOTAGENT_PORT}:${IOTAGENT_PORT}" # localhost:5000
- command: python agent.py server --host 0.0.0.0 --port ${IOTAGENT_PORT}
- healthcheck:
- test: curl -f https://localhost:${IOTAGENT_PORT}/version || exit 1
- interval: 5s
- timeout: 30s
- retries: 3
-
- orion:
- image: fiware/orion-ld:${ORION_LD_VERSION}
- hostname: orion
- container_name: fiware-orion
- depends_on:
- - mongo-db
- networks:
- - default
- ports:
- - "${ORION_LD_PORT}:${ORION_LD_PORT}" # localhost:1026
- command: -dbhost mongo-db -logLevel DEBUG -forwarding
- healthcheck:
- test: curl --fail -s http://orion:${ORION_LD_PORT}/version || exit 1
- interval: 5s
-
- mongo-db:
- image: mongo:${MONGO_DB_VERSION}
- hostname: mongo-db
- container_name: db-mongo
- expose:
- - "${MONGO_DB_PORT}"
- ports:
- - "${MONGO_DB_PORT}:${MONGO_DB_PORT}" # localhost:27017
- networks:
- - default
- volumes:
- - mongo-db:/data/db
- - mongo-config:/data/configdb
- healthcheck:
- test: |
- host=`hostname --ip-address || echo '127.0.0.1'`;
- mongo --quiet $host/test --eval 'quit(db.runCommand({ ping: 1 }).ok ? 0 : 2)' && echo 0 || echo 1
- interval: 5s
-
-
-networks:
- default:
- ipam:
- config:
- - subnet: 172.18.1.0/24
-
-volumes:
- mongo-db: ~
- mongo-config:
diff --git a/doc/API.md b/docs/API.md
similarity index 100%
rename from doc/API.md
rename to docs/API.md
diff --git a/doc/FIWARE IoTAgent-Turtle.postman_collection.json b/docs/FIWARE IoTAgent-Turtle.postman_collection.json
similarity index 100%
rename from doc/FIWARE IoTAgent-Turtle.postman_collection.json
rename to docs/FIWARE IoTAgent-Turtle.postman_collection.json
diff --git a/doc/SDMX to JSON-LD.drawio b/docs/SDMX to JSON-LD.drawio
similarity index 100%
rename from doc/SDMX to JSON-LD.drawio
rename to docs/SDMX to JSON-LD.drawio
diff --git a/docs/api.yaml b/docs/api.yaml
new file mode 100644
index 0000000..77623fb
--- /dev/null
+++ b/docs/api.yaml
@@ -0,0 +1,350 @@
+openapi: 3.0.3
+info:
+ description: 'Spec for the IoTAgent-Turtle agent'
+ version: 0.0.1
+ title: IoTAgentTurtle
+ contact:
+ email: fernando.lopez@fiware.org
+externalDocs:
+ description: Implementation on github.
+ url: 'https://github.com/flopezag/IoTAgent-Turtle'
+tags:
+ - name: Ops
+ description: Method to provide health info about the service information
+ - name: Parse
+ description: Perform the parse operation.
+paths:
+ '/version':
+ get:
+ tags:
+ - Ops
+ description: Provide the current health status and information vertsion of IoTAgent-Turtle.
+ operationId: getversion
+ responses:
+ '200':
+ description: Service is up and running and provide description information about the server.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/VersionInfo'
+
+ '/parse':
+ post:
+ tags:
+ - Parse
+ description: Parse a SDMX Turtle file into a NGSI-LD (JSON-LD) format and try to create all the entities found in the ngsi-ld Context Broker.
+ operationId: parse
+ requestBody:
+ required: true
+ content:
+ form-data:
+ schema:
+ $ref: '#/components/schemas/TurtleFile'
+ responses:
+ '201':
+ description: File succesfully read, parsed, and forwarded to the FIWARE Context Broker with several entities. For each of these entities a request to the Context Broker is performed. The result will be a json array containing an explanation to the result of every entity which was processed. The client application will be able to query if everything was properly created in the context broker reading each entry of the result array.
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ oneOf:
+ - $ref: '#/components/schemas/Http201ResponseInfo'
+ - $ref: '#/components/schemas/Http409ResponseInfo'
+ - $ref: '#/components/schemas/Http400ResponseInfo'
+ - $ref: '#/components/schemas/HttpOtherErrorsResponseInfo'
+ '500':
+ description: Problem connecting or accessing Context Broker.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Http500Error'
+components:
+ schemas:
+ Http201ResponseInfo:
+ type: object
+ description: Return status from CB to a given entity creation request
+ properties:
+ id:
+ type: string
+ example: "urn:ngsi-ld:Distribution:d2c32c6c6e637c0b319139ff25c1eff4"
+ status_code:
+ type: number
+ example: 201
+ description: The entity was created in the CB.
+ reason:
+ type: string
+ example: "Created"
+ Http400ResponseInfo:
+ type: object
+ description: Return status from CB to a given entity creation request
+ properties:
+ id:
+ type: string
+ example: "urn:ngsi-ld:AttributeProperty:a3018"
+ status_code:
+ type: number
+ example: 400
+ description: The format of the entity is not appropriate to the context broker, thus it is not created and HTTP/400 is returned.
+ reason:
+ type: string
+ example: "Bad Request"
+ Http409ResponseInfo:
+ type: object
+ description: Return status from CB to a given entity creation request
+ properties:
+ id:
+ type: string
+ example: "urn:ngsi-ld:Observation:obs-A-N-BE-W2-S1-S1-NA-B1G-_Z-A-_Z-XDC-V-N-2013"
+ status_code:
+ type: number
+ example: 409
+ description: The entity can't be created because it already exists in the Contest Broker.
+ reason:
+ type: string
+ example: "Conflict"
+ HttpOtherErrorsResponseInfo:
+ type: object
+ description: Return status from CB to a given entity creation request
+ properties:
+ id:
+ type: string
+ example: "urn:ngsi-ld:Concept:dissOrg"
+ status_code:
+ type: number
+ example: 422
+ description: The entity tryed to be creted in the context Broker shows other errors..
+ reason:
+ type: string
+ example: "Unprocessable entity"
+ description: Return reason will describe the problem with the creation of the entity in the context broker. It will be different to the previously explained ones and to the 500 error.
+ Http500Error:
+ type: object
+ description: Detailed error / problem with Context Broker
+ properties:
+ detail:
+ type: string
+ example: |
+ HTTPConnectionPool(host='orion-ld', port=1026): Max retries exceeded with url: /ngsi-ld/v1/entities (Caused by NameResolutionError(\": Failed to resolve 'orion-ld' ([Errno -5] Name has no usable address)\"))
+ VersionInfo:
+ type: object
+ description: Status of the running service
+ properties:
+ doc:
+ type: string
+ description: Link to the documentation of the component.
+ example: "UP"
+ git_hash:
+ type: string
+ description: Git hash of the current executed version.
+ example: "UP"
+ version:
+ type: string
+ description: Current version of the IoTAgent-Turtle server
+ example: "UP"
+ release_date:
+ type: string
+ description: Release date of the executed IoTAgent-Turtle server.
+ example: "UP"
+ uptime:
+ type: string
+ description: Time that the IoTAgent-Turtle server is up and running.
+ example: "UP"
+ required:
+ - doc
+ - git_hash
+ - version
+ - release_date
+ - uptime
+
+ TurtleFile:
+ type: string
+ format: binary
+ description: |
+ @prefix rdf: .
+
+ @prefix dc: .
+
+ @prefix dcterms: .
+
+ @prefix qb: .
+
+ @prefix rdfs: .
+
+ @prefix owl: .
+
+ @prefix skos: .
+
+ @prefix xsd: .
+
+ @prefix sdmx: .
+
+ @prefix sdmx-concept: .
+
+ @prefix sdmx-dimension: .
+
+ @prefix sdmx-attribute: .
+
+ @prefix sdmx-measure: .
+
+ @prefix sdmx-metadata: .
+
+ @prefix sdmx-code: .
+
+ @prefix sdmx-subject: .
+
+
+
+ a qb:DataSet ;
+
+ dcterms:issued "2022-04-01T08:00:00.000"^^xsd:dateTime ;
+
+ dcterms:publisher ;
+
+ dcterms:title "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr ;
+
+ qb:structure ;
+
+ rdfs:label "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr ;
+
+ sdmx-attribute:title "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr .
+
+
+
+ a qb:Observation;
+
+ ;
+
+ "W2" ;
+
+ "S1" ;
+
+ "S1" ;
+
+ "B" ;
+
+ "B1G" ;
+
+ "_Z" ;
+
+ "A" ;
+
+ "_Z" ;
+
+ "XDC" ;
+
+ "V" ;
+
+ "N" ;
+
+ qb:dataSet ;
+
+ sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+
+ sdmx-attribute:decimals sdmx-code:decimals-1 ;
+
+ sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+
+ sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+
+ sdmx-dimension:freq sdmx-code:freq-A ;
+
+ sdmx-dimension:refArea "BE" ;
+
+ sdmx-dimension:timePeriod "2011" ;
+
+ sdmx-measure:obsValue "1016.9"^^xsd:float .
+
+
+
+ a qb:Observation;
+
+ ;
+
+ "W2" ;
+
+ "S1" ;
+
+ "S1" ;
+
+ "B" ;
+
+ "B1G" ;
+
+ "_Z" ;
+
+ "A" ;
+
+ "_Z" ;
+
+ "XDC" ;
+
+ "V" ;
+
+ "N" ;
+
+ qb:dataSet ;
+
+ sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+
+ sdmx-attribute:decimals sdmx-code:decimals-1 ;
+
+ sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+
+ sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+
+ sdmx-dimension:freq sdmx-code:freq-A ;
+
+ sdmx-dimension:refArea "BE" ;
+
+ sdmx-dimension:timePeriod "2012" ;
+
+ sdmx-measure:obsValue "3016.9"^^xsd:float .
+
+
+
+ a qb:Observation;
+
+ ;
+
+ "W2" ;
+
+ "S1" ;
+
+ "S1" ;
+
+ "B" ;
+
+ "B1G" ;
+
+ "_Z" ;
+
+ "A" ;
+
+ "_Z" ;
+
+ "XDC" ;
+
+ "V" ;
+
+ "N" ;
+
+ qb:dataSet ;
+
+ sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+
+ sdmx-attribute:decimals sdmx-code:decimals-1 ;
+
+ sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+
+ sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+
+ sdmx-dimension:freq sdmx-code:freq-A ;
+
+ sdmx-dimension:refArea "BE" ;
+
+ sdmx-dimension:timePeriod "2013" ;
+
+ sdmx-measure:obsValue "9016.9"^^xsd:float .
+
+
diff --git a/examples/sep-dsd-2.ttl b/examples/sep-dsd-2.ttl
index 7228ce5..fd18a89 100644
--- a/examples/sep-dsd-2.ttl
+++ b/examples/sep-dsd-2.ttl
@@ -14,7 +14,7 @@
##################################################################################
# This DSD will be used for the DDI-CDI/NGSI-LD interoperability tests
-# It is a simple municipalitiy x sex x age group 3 dimensional cube
+# It is a simple municipality x sex x age group 3 dimensional cube
isc:dsd1
a qb:DataStructureDefinition ;
diff --git a/ngsild/__init__.py b/ngsild/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ngsild/ngsild_connector.py b/ngsild/ngsild_connector.py
new file mode 100644
index 0000000..51b03ab
--- /dev/null
+++ b/ngsild/ngsild_connector.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2023 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+#
+
+from pathlib import Path
+import json
+from requests import post, exceptions
+from fastapi import status
+from sdmx2jsonld.transform.parser import Parser
+from io import StringIO
+from logging import getLogger
+
+
+class NGSILDConnector:
+ def __init__(self, path=None):
+ self.logger = getLogger(__name__)
+ if path is None:
+ config_path = Path.cwd().joinpath("common/config.json")
+ else:
+ config_path = Path.cwd().joinpath(path)
+
+ with open(config_path) as config_file:
+ config = json.load(config_file)
+
+ self.base_url = config["broker"]
+
+ def get_url(self):
+ url = f"{self.base_url}/ngsi-ld/v1/entities"
+ return url
+
+ def send_data_array(self, json_object):
+ return_info = []
+ d = json.loads(json_object)
+ d = d if type(d) is list else [d]
+
+ for elem in d:
+ try:
+ rc, r = self.send_data(json.dumps(elem, indent=4))
+ return_info.append({"id": elem["id"], "status_code": rc, "reason": r})
+ except TypeError as e:
+ return_info.append(
+ {
+ "id": "UNK",
+ "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
+ "reason": e.args[0],
+ }
+ )
+ except Exception as e:
+ raise e
+ # reason = getattr(e, 'message', str(e))
+ # return_info.append({"id": "UNK",
+ # "status_code": 500,
+ # "reason": reason})
+
+ return return_info
+
+ def send_data(self, json_object):
+ # Send the data to a FIWARE Context Broker instance
+ headers = {
+ "Content-Type": "application/ld+json"
+ # , 'Accept': 'application/ld+json'
+ }
+
+ url = self.get_url()
+ resp = "..."
+
+ response = post(url=url, headers=headers, data=json_object, timeout=5)
+
+ # resp = json.loads(r.text)
+ response_status_code = response.status_code
+
+ if response_status_code == status.HTTP_201_CREATED:
+ self.logger.debug(f"LOCATION: {response.headers['Location']}")
+ if response_status_code == status.HTTP_400_BAD_REQUEST:
+ self.logger.info(f" Parser error: {response.reason}\n{json_object}")
+
+ # Let exceptions raise.... They can be controlled somewhere else.
+ return response_status_code, response.reason
+
+
+if __name__ == "__main__":
+ c = NGSILDConnector("../common/config.json")
+ print(c.get_url())
+
+ parser = Parser()
+ with open("../tests/files/structures-tourism.ttl", "r") as rf:
+ rdf_data = rf.read()
+
+ r = parser.parsing(StringIO(rdf_data), out=False)
+ c.send_data_array(r)
diff --git a/poetry.lock b/poetry.lock
old mode 100644
new mode 100755
index 52df914..e8d0430
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,3 +1,5 @@
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+
[[package]]
name = "anyio"
version = "3.6.2"
@@ -5,6 +7,10 @@ description = "High level compatibility layer for multiple asynchronous event lo
category = "main"
optional = false
python-versions = ">=3.6.2"
+files = [
+ {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
+ {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
+]
[package.dependencies]
idna = ">=2.8"
@@ -16,18 +22,16 @@ test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=
trio = ["trio (>=0.16,<0.22)"]
[[package]]
-name = "attrs"
-version = "22.1.0"
-description = "Classes Without Boilerplate"
+name = "cachetools"
+version = "5.3.1"
+description = "Extensible memoizing collections and decorators"
category = "dev"
optional = false
-python-versions = ">=3.5"
-
-[package.extras]
-dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
-docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
-tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+python-versions = ">=3.7"
+files = [
+ {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"},
+ {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
+]
[[package]]
name = "certifi"
@@ -36,6 +40,22 @@ description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
+files = [
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+]
+
+[[package]]
+name = "chardet"
+version = "5.1.0"
+description = "Universal encoding detector for Python 3"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"},
+ {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"},
+]
[[package]]
name = "charset-normalizer"
@@ -44,6 +64,10 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
category = "main"
optional = false
python-versions = ">=3.6.0"
+files = [
+ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
+ {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
+]
[package.extras]
unicode-backport = ["unicodedata2"]
@@ -55,6 +79,10 @@ description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
+ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
@@ -66,6 +94,10 @@ description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
[[package]]
name = "contextlib2"
@@ -74,6 +106,95 @@ description = "Backports and enhancements for the contextlib module"
category = "main"
optional = false
python-versions = ">=3.6"
+files = [
+ {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"},
+ {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.2.7"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+ {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+ {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+ {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+ {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+ {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+ {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+ {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+ {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+ {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+ {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+ {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+ {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+ {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+ {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+ {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+]
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "distlib"
+version = "0.3.7"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+]
[[package]]
name = "docopt"
@@ -82,6 +203,9 @@ description = "Pythonic argument parser, that will make you smile"
category = "main"
optional = false
python-versions = "*"
+files = [
+ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
+]
[[package]]
name = "exceptiongroup"
@@ -90,27 +214,49 @@ description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
+ {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
+]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
-version = "0.85.1"
+version = "0.100.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "fastapi-0.100.0-py3-none-any.whl", hash = "sha256:271662daf986da8fa98dc2b7c7f61c4abdfdccfb4786d79ed8b2878f172c6d5f"},
+ {file = "fastapi-0.100.0.tar.gz", hash = "sha256:acb5f941ea8215663283c10018323ba7ea737c571b67fc7e88e9469c7eb1d12e"},
+]
[package.dependencies]
-pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
-starlette = "0.20.4"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<3.0.0"
+starlette = ">=0.27.0,<0.28.0"
+typing-extensions = ">=4.5.0"
+
+[package.extras]
+all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+
+[[package]]
+name = "filelock"
+version = "3.12.2"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
+ {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
+]
[package.extras]
-all = ["email-validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"]
-dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "uvicorn[standard] (>=0.12.0,<0.19.0)"]
-doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.7.0)"]
-test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.971)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-orjson (==3.6.2)", "types-ujson (==5.4.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
[[package]]
name = "h11"
@@ -119,6 +265,10 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
+]
[[package]]
name = "hi-dateinfer"
@@ -127,6 +277,10 @@ description = "Infers date format from examples, by using a series of pattern ma
category = "main"
optional = false
python-versions = "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "hi-dateinfer-0.4.6.tar.gz", hash = "sha256:e2ded27aeb15ee6fcda0a30c2a341acbdaaa5b95c1c4db4c4680a4dbcda35c54"},
+ {file = "hi_dateinfer-0.4.6-py3-none-any.whl", hash = "sha256:46d27ccc875890315eec9d71d76c2306e84388c4a88106d2f768a921debb49e8"},
+]
[package.dependencies]
pytz = "*"
@@ -139,6 +293,10 @@ description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
+files = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
[[package]]
name = "iniconfig"
@@ -147,6 +305,10 @@ description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
+files = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
[[package]]
name = "isodate"
@@ -155,56 +317,90 @@ description = "An ISO 8601 date/time/duration parser and formatter"
category = "main"
optional = false
python-versions = "*"
+files = [
+ {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"},
+ {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"},
+]
[package.dependencies]
six = "*"
[[package]]
name = "lark"
-version = "1.1.3"
+version = "1.1.7"
description = "a modern parsing library"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.6"
+files = [
+ {file = "lark-1.1.7-py3-none-any.whl", hash = "sha256:9e5dc5bbf93fa1840083707285262514a0ef8a6613874af7ea1cec60468d6e92"},
+ {file = "lark-1.1.7.tar.gz", hash = "sha256:be7437bf1f37ab08b355f29ff2571d77d777113d0a8c4352b0c513dced6c5a1e"},
+]
[package.extras]
atomic-cache = ["atomicwrites"]
+interegular = ["interegular (>=0.3.1,<0.4.0)"]
nearley = ["js2py"]
regex = ["regex"]
[[package]]
name = "loguru"
-version = "0.6.0"
+version = "0.7.0"
description = "Python logging made (stupidly) simple"
category = "main"
optional = false
python-versions = ">=3.5"
+files = [
+ {file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"},
+ {file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"},
+]
[package.dependencies]
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
-dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"]
+dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v0.990)", "pre-commit (==3.2.1)", "pytest (==6.1.2)", "pytest (==7.2.1)", "pytest-cov (==2.12.1)", "pytest-cov (==4.0.0)", "pytest-mypy-plugins (==1.10.1)", "pytest-mypy-plugins (==1.9.3)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.2.0)", "tox (==3.27.1)", "tox (==4.4.6)"]
[[package]]
name = "packaging"
-version = "21.3"
+version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+]
-[package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+[[package]]
+name = "platformdirs"
+version = "3.9.1"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.9.1-py3-none-any.whl", hash = "sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f"},
+ {file = "platformdirs-3.9.1.tar.gz", hash = "sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
[[package]]
name = "pluggy"
-version = "1.0.0"
+version = "1.2.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+files = [
+ {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"},
+ {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"},
+]
[package.extras]
dev = ["pre-commit", "tox"]
@@ -217,6 +413,44 @@ description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
+ {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
+ {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
+ {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
+ {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
+ {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
+ {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
+ {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
+ {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
+ {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
+ {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
+ {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
+ {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
+ {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
+ {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
+ {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
+ {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
+ {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
+ {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
+ {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
+ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
+ {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
+]
[package.dependencies]
typing-extensions = ">=4.1.0"
@@ -232,20 +466,47 @@ description = "pyparsing module - Classes and methods to define and execute pars
category = "main"
optional = false
python-versions = ">=3.6.8"
+files = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
+[[package]]
+name = "pyproject-api"
+version = "1.5.3"
+description = "API to interact with the python pyproject.toml based projects"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyproject_api-1.5.3-py3-none-any.whl", hash = "sha256:14cf09828670c7b08842249c1f28c8ee6581b872e893f81b62d5465bec41502f"},
+ {file = "pyproject_api-1.5.3.tar.gz", hash = "sha256:ffb5b2d7cad43f5b2688ab490de7c4d3f6f15e0b819cb588c4b771567c9729eb"},
+]
+
+[package.dependencies]
+packaging = ">=23.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "setuptools (>=67.8)", "wheel (>=0.40)"]
+
[[package]]
name = "pytest"
-version = "7.2.0"
+version = "7.4.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"},
+ {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"},
+]
[package.dependencies]
-attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
@@ -254,18 +515,37 @@ pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+
+[package.dependencies]
+six = ">=1.5"
[[package]]
name = "python-multipart"
-version = "0.0.5"
+version = "0.0.6"
description = "A streaming multipart parser for Python"
category = "main"
optional = false
-python-versions = "*"
+python-versions = ">=3.7"
+files = [
+ {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"},
+ {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"},
+]
-[package.dependencies]
-six = ">=1.4.0"
+[package.extras]
+dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"]
[[package]]
name = "pytz"
@@ -274,41 +554,50 @@ description = "World timezone definitions, modern and historical"
category = "main"
optional = false
python-versions = "*"
+files = [
+ {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
+ {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
+]
[[package]]
name = "rdflib"
-version = "6.2.0"
+version = "6.3.2"
description = "RDFLib is a Python library for working with RDF, a simple yet powerful language for representing information."
category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.7,<4.0"
+files = [
+ {file = "rdflib-6.3.2-py3-none-any.whl", hash = "sha256:36b4e74a32aa1e4fa7b8719876fb192f19ecd45ff932ea5ebbd2e417a0247e63"},
+ {file = "rdflib-6.3.2.tar.gz", hash = "sha256:72af591ff704f4caacea7ecc0c5a9056b8553e0489dd4f35a9bc52dbd41522e0"},
+]
[package.dependencies]
-isodate = "*"
-pyparsing = "*"
-setuptools = "*"
+isodate = ">=0.6.0,<0.7.0"
+pyparsing = ">=2.1.0,<4"
[package.extras]
-berkeleydb = ["berkeleydb"]
-dev = ["black (==22.6.0)", "flake8", "flakeheaven", "isort", "mypy", "pep8-naming", "types-setuptools"]
-docs = ["myst-parser", "sphinx (<6)", "sphinx-autodoc-typehints", "sphinxcontrib-apidoc", "sphinxcontrib-kroki"]
-html = ["html5lib"]
-networkx = ["networkx"]
-tests = ["html5lib", "pytest", "pytest-cov"]
+berkeleydb = ["berkeleydb (>=18.1.0,<19.0.0)"]
+html = ["html5lib (>=1.0,<2.0)"]
+lxml = ["lxml (>=4.3.0,<5.0.0)"]
+networkx = ["networkx (>=2.0.0,<3.0.0)"]
[[package]]
name = "requests"
-version = "2.28.1"
+version = "2.31.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
-python-versions = ">=3.7, <4"
+python-versions = ">=3.7"
+files = [
+ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+ {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
[package.dependencies]
certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<3"
+charset-normalizer = ">=2,<4"
idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<1.27"
+urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
@@ -321,6 +610,10 @@ description = "Simple data validation library"
category = "main"
optional = false
python-versions = "*"
+files = [
+ {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"},
+ {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"},
+]
[package.dependencies]
contextlib2 = ">=0.5.5"
@@ -332,19 +625,10 @@ description = "A lightweight package that adds security headers for Python web f
category = "main"
optional = false
python-versions = ">=3.6"
-
-[[package]]
-name = "setuptools"
-version = "65.5.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+files = [
+ {file = "secure-0.3.0-py3-none-any.whl", hash = "sha256:a93b720c7614809c131ca80e477263140107c6c212829d0a6e1f7bc8d859c608"},
+ {file = "secure-0.3.0.tar.gz", hash = "sha256:6e30939d8f95bf3b8effb8a36ebb5ed57f265daeeae905e3aa9677ea538ab64e"},
+]
[[package]]
name = "six"
@@ -353,6 +637,10 @@ description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
[[package]]
name = "sniffio"
@@ -361,20 +649,29 @@ description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
+ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+]
[[package]]
name = "starlette"
-version = "0.20.4"
+version = "0.27.0"
description = "The little ASGI library that shines."
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"},
+ {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"},
+]
[package.dependencies]
anyio = ">=3.4.0,<5"
+typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
-full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"]
+full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"]
[[package]]
name = "tomli"
@@ -383,14 +680,113 @@ description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tox"
+version = "4.6.4"
+description = "tox is a generic virtualenv management and test command line tool"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tox-4.6.4-py3-none-any.whl", hash = "sha256:1b8f8ae08d6a5475cad9d508236c51ea060620126fd7c3c513d0f5c7f29cc776"},
+ {file = "tox-4.6.4.tar.gz", hash = "sha256:5e2ad8845764706170d3dcaac171704513cc8a725655219acb62fe4380bdadda"},
+]
+
+[package.dependencies]
+cachetools = ">=5.3.1"
+chardet = ">=5.1"
+colorama = ">=0.4.6"
+filelock = ">=3.12.2"
+packaging = ">=23.1"
+platformdirs = ">=3.8"
+pluggy = ">=1.2"
+pyproject-api = ">=1.5.2"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+virtualenv = ">=20.23.1"
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.3,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
+
+[[package]]
+name = "types-docopt"
+version = "0.6.11.3"
+description = "Typing stubs for docopt"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-docopt-0.6.11.3.tar.gz", hash = "sha256:ee6db04587900d94f4c137f9b191cc72ca8920a2bd7f823e27b72cb8da0d9d3b"},
+ {file = "types_docopt-0.6.11.3-py3-none-any.whl", hash = "sha256:6836fbc5f4f622a0a97f9dea03f62a387e03157e6559e3086f49be0ca089590a"},
+]
+
+[[package]]
+name = "types-python-dateutil"
+version = "2.8.19.14"
+description = "Typing stubs for python-dateutil"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"},
+ {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"},
+]
+
+[[package]]
+name = "types-pytz"
+version = "2023.3.0.0"
+description = "Typing stubs for pytz"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"},
+ {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"},
+]
+
+[[package]]
+name = "types-requests"
+version = "2.31.0.2"
+description = "Typing stubs for requests"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-requests-2.31.0.2.tar.gz", hash = "sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40"},
+ {file = "types_requests-2.31.0.2-py3-none-any.whl", hash = "sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a"},
+]
+
+[package.dependencies]
+types-urllib3 = "*"
+
+[[package]]
+name = "types-urllib3"
+version = "1.26.25.14"
+description = "Typing stubs for urllib3"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"},
+ {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"},
+]
[[package]]
name = "typing-extensions"
-version = "4.4.0"
+version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+]
[[package]]
name = "urllib3"
@@ -399,6 +795,10 @@ description = "HTTP library with thread-safe connection pooling, file post, and
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+files = [
+ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
+ {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
+]
[package.extras]
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
@@ -407,18 +807,44 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "uvicorn"
-version = "0.19.0"
+version = "0.23.1"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+ {file = "uvicorn-0.23.1-py3-none-any.whl", hash = "sha256:1d55d46b83ee4ce82b4e82f621f2050adb3eb7b5481c13f9af1744951cae2f1f"},
+ {file = "uvicorn-0.23.1.tar.gz", hash = "sha256:da9b0c8443b2d7ee9db00a345f1eee6db7317432c9d4400f5049cc8d358383be"},
+]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
+typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.24.1"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.24.1-py3-none-any.whl", hash = "sha256:01aacf8decd346cf9a865ae85c0cdc7f64c8caa07ff0d8b1dfc1733d10677442"},
+ {file = "virtualenv-20.24.1.tar.gz", hash = "sha256:2ef6a237c31629da6442b0bcaa3999748108c7166318d1f55cc9f8d7294e97bd"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.12,<4"
+platformdirs = ">=3.5.1,<4"
[package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"]
+docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
[[package]]
name = "wheel"
@@ -427,6 +853,10 @@ description = "A built-package format for Python"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"},
+ {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"},
+]
[package.extras]
test = ["pytest (>=3.0.0)", "pytest-cov"]
@@ -438,197 +868,15 @@ description = "A small Python utility to set file creation time on Windows"
category = "main"
optional = false
python-versions = ">=3.5"
+files = [
+ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
+ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
+]
[package.extras]
dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[metadata]
-lock-version = "1.1"
-python-versions = "^3.10"
-content-hash = "efb208b9ebb656a11935975f1f40e583c2c4ac285bbb8a5509f560da21396f7e"
-
-[metadata.files]
-anyio = [
- {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
- {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
-]
-attrs = [
- {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
- {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
-]
-certifi = [
- {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
- {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
-]
-charset-normalizer = [
- {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
- {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
-]
-click = [
- {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
- {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
-]
-colorama = [
- {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
- {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
-]
-contextlib2 = [
- {file = "contextlib2-21.6.0-py2.py3-none-any.whl", hash = "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f"},
- {file = "contextlib2-21.6.0.tar.gz", hash = "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869"},
-]
-docopt = [
- {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
-]
-exceptiongroup = [
- {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"},
- {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"},
-]
-fastapi = [
- {file = "fastapi-0.85.1-py3-none-any.whl", hash = "sha256:de3166b6b1163dc22da4dc4ebdc3192fcbac7700dd1870a1afa44de636a636b5"},
- {file = "fastapi-0.85.1.tar.gz", hash = "sha256:1facd097189682a4ff11cbd01334a992e51b56be663b2bd50c2c09523624f144"},
-]
-h11 = [
- {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
- {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
-]
-hi-dateinfer = [
- {file = "hi-dateinfer-0.4.6.tar.gz", hash = "sha256:e2ded27aeb15ee6fcda0a30c2a341acbdaaa5b95c1c4db4c4680a4dbcda35c54"},
- {file = "hi_dateinfer-0.4.6-py3-none-any.whl", hash = "sha256:46d27ccc875890315eec9d71d76c2306e84388c4a88106d2f768a921debb49e8"},
-]
-idna = [
- {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
- {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
-]
-iniconfig = [
- {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
- {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
-isodate = [
- {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"},
- {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"},
-]
-lark = [
- {file = "lark-1.1.3-py3-none-any.whl", hash = "sha256:45cd8b4d8b0487863f0f4cf3c305a1293db86af576d4a62cfb572e57a9b609e5"},
- {file = "lark-1.1.3.tar.gz", hash = "sha256:ca0d1aeb57f434c7276d209729e92b0e5017d177dde553134760c35bb4647d11"},
-]
-loguru = [
- {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"},
- {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"},
-]
-packaging = [
- {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
- {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
-]
-pluggy = [
- {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
- {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
-]
-pydantic = [
- {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
- {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
- {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
- {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
- {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
- {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
- {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
- {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
- {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
- {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
- {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
- {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
- {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
- {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
- {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
- {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
- {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
- {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
- {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
- {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
- {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
- {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
- {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
- {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
- {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
- {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
- {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
- {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
- {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
- {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
- {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
- {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
- {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
- {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
- {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
- {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
-]
-pyparsing = [
- {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
- {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-pytest = [
- {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
- {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
-]
-python-multipart = [
- {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"},
-]
-pytz = [
- {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
- {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
-]
-rdflib = [
- {file = "rdflib-6.2.0-py3-none-any.whl", hash = "sha256:85c34a86dfc517a41e5f2425a41a0aceacc23983462b32e68610b9fad1383bca"},
- {file = "rdflib-6.2.0.tar.gz", hash = "sha256:62dc3c86d1712db0f55785baf8047f63731fa59b2682be03219cb89262065942"},
-]
-requests = [
- {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
- {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
-]
-schema = [
- {file = "schema-0.7.5-py2.py3-none-any.whl", hash = "sha256:f3ffdeeada09ec34bf40d7d79996d9f7175db93b7a5065de0faa7f41083c1e6c"},
- {file = "schema-0.7.5.tar.gz", hash = "sha256:f06717112c61895cabc4707752b88716e8420a8819d71404501e114f91043197"},
-]
-secure = [
- {file = "secure-0.3.0-py3-none-any.whl", hash = "sha256:a93b720c7614809c131ca80e477263140107c6c212829d0a6e1f7bc8d859c608"},
- {file = "secure-0.3.0.tar.gz", hash = "sha256:6e30939d8f95bf3b8effb8a36ebb5ed57f265daeeae905e3aa9677ea538ab64e"},
-]
-setuptools = [
- {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"},
- {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"},
-]
-six = [
- {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
- {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
-]
-sniffio = [
- {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
- {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
-]
-starlette = [
- {file = "starlette-0.20.4-py3-none-any.whl", hash = "sha256:c0414d5a56297d37f3db96a84034d61ce29889b9eaccf65eb98a0b39441fcaa3"},
- {file = "starlette-0.20.4.tar.gz", hash = "sha256:42fcf3122f998fefce3e2c5ad7e5edbf0f02cf685d646a83a08d404726af5084"},
-]
-tomli = [
- {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
- {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-typing-extensions = [
- {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
- {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
-]
-urllib3 = [
- {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
- {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
-]
-uvicorn = [
- {file = "uvicorn-0.19.0-py3-none-any.whl", hash = "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f"},
- {file = "uvicorn-0.19.0.tar.gz", hash = "sha256:cf538f3018536edb1f4a826311137ab4944ed741d52aeb98846f52215de57f25"},
-]
-wheel = [
- {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"},
- {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"},
-]
-win32-setctime = [
- {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"},
- {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"},
-]
+lock-version = "2.0"
+python-versions = ">=3.8,<4.0"
+content-hash = "604633e797026198f8c6db0da6791c4bfefe2a8e8501de135aa6c03bc76f669c"
diff --git a/pyproject.toml b/pyproject.toml
index 95556a6..967a5f8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "SDMX2JSON-LD"
-version = "0.5.2"
+version = "1.0.1"
description = "A SDMX in RDF Turtle 1.1 format parser to generate valid JSON-LD and send to FIWARE Context Brokers using ETSI NGSI-LD."
authors = ["Fernando López "]
readme = "./sdmx2jsonld/README.md"
@@ -15,30 +15,71 @@ classifiers = [
repository = "https://github.com/flopezag/IoTAgent-Turtle"
-
[tool.poetry.dependencies]
-python = "^3.7"
-lark = "1.1.3"
+python = "3.10, 3.11"
+lark = "1.1.7"
secure = "0.3.0"
docopt = "0.6.2"
schema = "0.7.5"
hi-dateinfer = "0.4.6"
-fastapi = "0.85.1"
-uvicorn = "0.19.0"
-python-multipart = "0.0.5"
-loguru = "0.6.0"
-requests = "2.28.1"
-rdflib = "~6.2.0"
+fastapi = "0.100.0"
+uvicorn = "0.23.1"
+python-multipart = "0.0.6"
+loguru = "0.7.0"
+requests = "2.31.0"
+rdflib = "6.3.2"
+python-dateutil = "2.8.2"
+
+[tool.black]
+line-length = 120
[tool.poetry.dev-dependencies]
-pytest = "*"
+pytest = "7.4.0"
+tox = "4.6.4"
+types-docopt = "0.6.11.3"
+types-python-dateutil = "2.8.19.14"
+types-pytz = "2023.3.0.0"
+types-requests = "2.31.0.2"
+coverage = "7.2.7"
[build-system]
-requires = ["poetry-core"]
+requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
-
[[tool.poetry_bumpversion.replacements]]
files = ["cli/command.py", "sdmx2jsonld/transform/parser.py"]
search = '__version__ = "{current_version}"'
replace = '__version__ = "{new_version}"'
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "--tb=auto -ra --showlocals"
+
+[tool.coverage]
+html.show_contexts = true
+html.skip_covered = false
+paths.source = [
+ "api",
+ "cli",
+ "common",
+ "ngsild",
+ "sdmx2jsonld",
+]
+report.fail_under = 88
+run.parallel = true
+run.plugins = ["covdefaults"]
+
+[tool.mypy]
+python_version = "3.11"
+show_error_codes = true
+
+[[tool.mypy.overrides]]
+module = [
+ "api",
+ "cli",
+ "common",
+ "ngsild",
+ "sdmx2jsonld",
+]
+ignore_missing_imports = true
+warn_unused_ignores = true
diff --git a/requirements.txt b/requirements.txt
index 9e5b0e2..21a0aa2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,13 @@
-# python3.10, virtualenv -ppython3.10 .venv
-lark==1.1.3
+# python3.11, virtualenv -ppython3.11 .venv
+lark==1.1.7
secure==0.3.0
docopt==0.6.2
schema==0.7.5
hi-dateinfer==0.4.6
-fastapi==0.85.1
-uvicorn==0.19.0
-python-multipart==0.0.5
-loguru==0.6.0
-requests==2.28.1
-rdflib~=6.2.0
-
+fastapi==0.100.0
+uvicorn==0.23.1
+python-multipart==0.0.6
+loguru==0.7.0
+requests==2.31.0
+rdflib==6.3.2
+python-dateutil==2.8.2
diff --git a/sdmx2jsonld/common/__init__.py b/sdmx2jsonld/common/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdmx2jsonld/common/classprecedence.py b/sdmx2jsonld/common/classprecedence.py
index bf642c9..8cd6936 100644
--- a/sdmx2jsonld/common/classprecedence.py
+++ b/sdmx2jsonld/common/classprecedence.py
@@ -22,15 +22,15 @@
entities = {
- 'qb:DataStructureDefinition': 'Dataset',
- 'qb:ComponentSpecification': 'Component',
- 'qb:AttributeProperty': 'Attribute',
- 'qb:DimensionProperty': 'Dimension',
- 'qb:CodedProperty': 'Dimension',
- 'rdfs:Class': 'Class',
- 'owl:Class': 'Class',
- 'skos:ConceptScheme': 'ConceptScheme',
- 'skos:Concept': 'Range'
+ "qb:DataStructureDefinition": "Dataset",
+ "qb:ComponentSpecification": "Component",
+ "qb:AttributeProperty": "Attribute",
+ "qb:DimensionProperty": "Dimension",
+ "qb:CodedProperty": "Dimension",
+ "rdfs:Class": "Class",
+ "owl:Class": "Class",
+ "skos:ConceptScheme": "ConceptScheme",
+ "skos:Concept": "Range",
}
@@ -47,7 +47,7 @@ def __init__(self):
"skos:Concept": 40,
"rdfs:Class": 20,
"owl:Class": 20,
- "qb:SliceKey": 10
+ "qb:SliceKey": 10,
}
def get_value(self, aclass: str) -> int:
@@ -60,25 +60,24 @@ def precedence(self, data: list) -> str:
classes_values = list(map(lambda x: self.get_value(x), data))
# We need to check if all element of the list are the value 250 because could not be possible to have at
- # the same time a DimensionProperty and AttributeProperty, this is an ERROR and we need to report.
+ # the same time a DimensionProperty and AttributeProperty, this is an ERROR therefore we need to report it.
result = all(element == 250 for element in classes_values) and len(data) > 1
if result is True:
- raise ClassesPrecedencePropertyError(data)
+ raise ClassPrecedencePropertyError(data)
# In case that we have several values identical of type Class, we need to report a WARNING message because maybe
- # it is not needed multitype in that case.
+ # it is not needed multi-type in that case.
result = all(element == 20 for element in classes_values) and len(data) > 1
if result is True:
- raise ClassesPrecedenceClassError(data)
+ raise ClassPrecedenceClassError(data)
- # In other chase, we return the max value of the list
- aux = max(classes_values)
- aux = data[classes_values.index(aux)]
+ # In other case, we return the max value of the list
+ aux = data[classes_values.index(max(classes_values))]
return aux
-class ClassesPrecedenceError(Exception):
+class ClassPrecedenceError(Exception):
"""Base class for other exceptions"""
def __init__(self, data, message):
@@ -86,15 +85,16 @@ def __init__(self, data, message):
self.data = data
def __str__(self):
- return f'{self.data} -> {self.message}'
+ return f"{self.data} -> {self.message}"
-class ClassesPrecedencePropertyError(ClassesPrecedenceError):
+class ClassPrecedencePropertyError(ClassPrecedenceError):
"""Raised when the input value is too small"""
- """Exception raised for errors in the input salary.
+
+ """Exception raised for errors in the input data.
Attributes:
- salary -- input salary which caused the error
+ data -- input data which caused the error
message -- explanation of the error
"""
@@ -102,12 +102,13 @@ def __init__(self, data, message="Incompatible multiclass definition"):
super().__init__(data=data, message=message)
-class ClassesPrecedenceClassError(ClassesPrecedenceError):
+class ClassPrecedenceClassError(ClassPrecedenceError):
"""Raised when the input value is too large"""
- """Exception raised for errors in the input salary.
+
+ """Exception raised for errors in the input data.
Attributes:
- salary -- input salary which caused the error
+ data -- input data which caused the error
message -- explanation of the error
"""
@@ -115,6 +116,6 @@ def __init__(self, data, message="Possible redundant Class definition"):
super().__init__(data=data, message=message)
-if __name__ == '__main__':
+if __name__ == "__main__":
pre = Precedence()
- obtained = pre.precedence(['qb:DataStructureDefinition'])
+ obtained = pre.precedence(["qb:DataStructureDefinition"])
diff --git a/sdmx2jsonld/common/commonclass.py b/sdmx2jsonld/common/commonclass.py
index 3aea878..8392947 100644
--- a/sdmx2jsonld/common/commonclass.py
+++ b/sdmx2jsonld/common/commonclass.py
@@ -33,18 +33,18 @@ def __init__(self, entity):
def add_context(self, context, context_mapping):
# Set the context as it is received and mixed with the core context
- self.data['@context'] = context['@context']
+ self.data["@context"] = context["@context"]
# Fix the prefix of the core properties of the Dataset entity
new_data = dict()
for k, v in self.data.items():
# Return if the string matched the ReGex
- out = k.split(':')
+ out = k.split(":")
if len(out) == 2 and out[0] in context_mapping.keys():
new_prefix = context_mapping[out[0]]
- new_key = new_prefix + ':' + out[1]
+ new_key = new_prefix + ":" + out[1]
new_data[new_key] = self.data[k]
self.keys[k] = new_key
@@ -53,18 +53,21 @@ def add_context(self, context, context_mapping):
self.data = new_data
+ def get(self):
+ return self.data
+
def save(self):
data = self.get()
- aux = data['id'].split(":")
+ aux = data["id"].split(":")
length_aux = len(aux)
# We need to check that the output folder exist
- if exists('./output') is False:
- # We need to create the folder because it does not exits
- mkdir('./output')
+ if exists("./output") is False:
+ # We need to create the folder because it does not exist
+ mkdir("./output")
- filename = './output/' + '_'.join(aux[length_aux - 2:]) + '.jsonld'
+ filename = "./output/" + "_".join(aux[length_aux - 2 :]) + ".jsonld"
# Serializing json
json_object = dumps(data, indent=4, ensure_ascii=False)
@@ -73,13 +76,22 @@ def save(self):
with open(filename, "w") as outfile:
outfile.write(json_object)
- def generate_id(self, value, entity=None):
+ def generate_id(self, value, entity=None, update_id=False):
parse = RegParser()
aux = parse.obtain_id(value)
if entity is None:
- aux = "urn:ngsi-ld:" + self.entity + ":" + aux
+ new_aux = "urn:ngsi-ld:" + self.entity + ":" + aux
+ else:
+ new_aux = "urn:ngsi-ld:" + entity + ":" + aux
+
+ if update_id:
+ self.data["id"] = new_aux
+ return new_aux
else:
- aux = "urn:ngsi-ld:" + entity + ":" + aux
+ return aux, new_aux
+
+ def __generate_property__(self, key, value):
+ result = {key: {"type": "Property", "value": value}}
- return aux
+ return result
diff --git a/sdmx2jsonld/common/config.py b/sdmx2jsonld/common/config.py
index 84b50b5..fb80855 100644
--- a/sdmx2jsonld/common/config.py
+++ b/sdmx2jsonld/common/config.py
@@ -1,15 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
from os.path import join, exists, dirname, abspath
# Settings file is inside Basics directory, therefore I have to go back to the parent directory
# to have the Code Home directory
MODULEHOME = dirname(dirname(abspath(__file__)))
-GRAMMARFOLDER = join(MODULEHOME, 'grammar')
-GRAMMARFILE = join(GRAMMARFOLDER, 'grammar.lark')
+GRAMMARFOLDER = join(MODULEHOME, "grammar")
+GRAMMARFILE = join(GRAMMARFOLDER, "grammar.lark")
if not exists(GRAMMARFILE):
- msg = '\nERROR: There is not Lark grammar file in the expected folder. ' \
- '\n Unable to parse the RDF Turtle file.' \
- '\n\n Please correct it if you do not want to see these messages.\n\n\n'
+ msg = (
+ "\nERROR: There is not Lark grammar file in the expected folder. "
+ "\n Unable to parse the RDF Turtle file."
+ "\n\n Please correct it if you do not want to see these messages.\n\n\n"
+ )
print(msg)
diff --git a/sdmx2jsonld/common/datatypeconversion.py b/sdmx2jsonld/common/datatypeconversion.py
index 6b6f8a0..f1ad7a7 100644
--- a/sdmx2jsonld/common/datatypeconversion.py
+++ b/sdmx2jsonld/common/datatypeconversion.py
@@ -19,18 +19,23 @@
# License for the specific language governing permissions and limitations
# under the License.
##
-from hidateinfer import infer
+from hidateinfer import infer # type: ignore[import]
from datetime import datetime, timezone
from re import compile, sub
+from dateutil import parser
+import pytz
+from sdmx2jsonld.common.tzinfos import whois_timezone_info
class DataTypeConversion:
def __init__(self):
self.types = {
- 'xsd:dateTime': 'stodt',
- 'xsd:int': 'stoi',
- 'xsd:boolean': 'stob'
+ "xsd:dateTime": "stoutc",
+ "xsd:int": "stoi",
+ "xsd:boolean": "stob",
+ "xsd:float": "stof",
}
+
self.regex_12hour = compile(r"(^.*T%)(I)(.*)$")
self.regex_microseconds = compile(r"^(.*T%.*:%S\.)(%H)*$")
self.regex_microseconds2 = compile(r"^(.*T%.*:%S\.)(%y)*$")
@@ -38,96 +43,131 @@ def __init__(self):
self.regex_false_date2 = compile(r"^%Y-%d-%m(.*)%f")
def correct_datatype_format(self, format_dt: str, hour24: bool = True):
-
if hour24:
format_dt = sub(self.regex_12hour, r"\1H\3", format_dt)
- format_dt = sub(self.regex_microseconds, r"\1%f", format_dt)
+ format_dt = sub(self.regex_microseconds, r"\1%f", format_dt)
format_dt = sub(self.regex_microseconds2, r"\1%f", format_dt)
- format_dt = sub(self.regex_false_date, r"%Y-%m-%d\1%f", format_dt)
+ format_dt = sub(self.regex_false_date, r"%Y-%m-%d\1%f", format_dt)
format_dt = sub(self.regex_false_date2, r"%Y-%m-%d\1%f", format_dt)
return format_dt
def convert(self, data, datatype):
+ def stoutc(value):
+ """
+ Converts a date in string format to UTC date using
+ """
+ dt = parser.parse(value, tzinfos=whois_timezone_info)
+ dt = dt.astimezone(pytz.UTC)
+ return dt.replace(tzinfo=timezone.utc).isoformat()
+
def stodt(value):
- # print(f'toDateTime function, arguments {value}')
if isinstance(value, str):
result = infer([value])
elif isinstance(value, list):
result = infer(value)
else:
- raise Exception(f'Invalid format received: {type(value)}')
+ raise Exception(f"Invalid format received: {type(value)}")
result = self.correct_datatype_format(result)
-
- # print(f'format {result}')
result = datetime.strptime(value, result).replace(tzinfo=timezone.utc).isoformat()
- # print(f'result {result}')
+
return result
def stoi(value):
"""
- Converts 'something' to int. Raises exception for invalid formats
+ Converts 'something' to int. Raises exception for invalid formats
"""
if isinstance(value, str):
- result = value.replace('"', '')
+ result = value.replace('"', "")
elif isinstance(value, int):
result = value
else:
- raise Exception(f'Invalid format received: {type(value)}')
+ raise Exception(f"Invalid format received: {type(value)}")
return int(result)
- def stob(value):
+ def stof(value):
"""
- Converts 'something' to boolean. Raises exception for invalid formats
- Possible True values: 1, True, "1", "TRue", "yes", "y", "t"
- Possible False values: 0, False, None, [], {}, "", "0", "faLse", "no", "n", "f", 0.0, ...
+ Converts 'something' to float. Raises exception for invalid formats
"""
- print(f'toBool function, arguments {value}')
+ if isinstance(value, str):
+ result = value.replace('"', "")
+ elif isinstance(value, float):
+ result = value
+ else:
+ raise Exception(f"Invalid format received: {type(value)}")
+
+ return float(result)
+ def stob(value):
+ """
+ Converts 'something' to boolean. Raises exception for invalid formats
+ Possible True values: 1, True, "1", "TRue", "yes", "y", "t"
+ Possible False values: 0, False, None, [], {}, "", "0", "faLse", "no", "n", "f", 0.0, ...
+ """
if str(value).lower() in ("yes", "y", "true", "t", "1"):
return True
- if str(value).lower() in ("no", "n", "false", "f", "0", "0.0", "", "none", "[]", "{}"):
+ if str(value).lower() in (
+ "no",
+ "n",
+ "false",
+ "f",
+ "0",
+ "0.0",
+ "",
+ "none",
+ "[]",
+ "{}",
+ ):
return False
# logger.error(f'Invalid value for boolean conversion: {str(value)}')
- raise Exception(f'Invalid value for boolean conversion: {str(value)}')
+ raise Exception(f"Invalid value for boolean conversion: {str(value)}")
try:
- function = self.types[datatype] + '(value=' + data + ')'
+ # jicg - function = self.types[datatype] + f'(value="{data}")'
+ function = self.types[datatype] + "(value=" + data + ")"
return eval(function)
except KeyError:
# logger.error(f'Datatype not defined: {datatype}')
- print(f'Datatype not defined: {datatype}')
- raise Exception(f'Datatype not defined: {datatype}')
+ print(f"Datatype not defined: {datatype}")
+ raise Exception(f"Datatype not defined: {datatype}")
except NameError:
# logger.error(f"name '{data}' is not defined")
print(f"name '{data}' is not defined")
raise Exception(f"name '{data}' is not defined")
-if __name__ == '__main__':
+if __name__ == "__main__":
from lark import Token
- data1 = ['"2022-01-15T08:00:00.000"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
- data2 = ['"2"', Token('FORMATCONNECTOR', '^^'), 'xsd:int']
- data22 = ['2', Token('FORMATCONNECTOR', '^^'), 'xsd:int']
- data23 = ['asdfs', Token('FORMATCONNECTOR', '^^'), 'xsd:int']
- data3 = ['"true"', Token('FORMATCONNECTOR', '^^'), 'xsd:boolean']
- data4 = ['"fake"', Token('FORMATCONNECTOR', '^^'), 'otraCosa']
- data5 = ['"2022-01-10T09:00:00.000"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
- data6 = ['"2021-07-01T11:50:37.3"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
- data7 = ['"2021-09-28T15:31:24.05"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
-
- print(infer(['Mon Jan 13 09:52:52 MST 2014']))
+ data1 = [
+ '"2022-01-15T08:00:00.000"',
+ Token("FORMATCONNECTOR", "^^"),
+ "xsd:dateTime",
+ ]
+ data2 = ['"2"', Token("FORMATCONNECTOR", "^^"), "xsd:int"]
+ data22 = ["2", Token("FORMATCONNECTOR", "^^"), "xsd:int"]
+ data23 = ["asdfs", Token("FORMATCONNECTOR", "^^"), "xsd:int"]
+ data3 = ['"true"', Token("FORMATCONNECTOR", "^^"), "xsd:boolean"]
+ data4 = ['"fake"', Token("FORMATCONNECTOR", "^^"), "otraCosa"]
+ data5 = [
+ '"2022-01-10T09:00:00.000"',
+ Token("FORMATCONNECTOR", "^^"),
+ "xsd:dateTime",
+ ]
+ data6 = ['"2021-07-01T11:50:37.3"', Token("FORMATCONNECTOR", "^^"), "xsd:dateTime"]
+ data7 = ['"2021-09-28T15:31:24.05"', Token("FORMATCONNECTOR", "^^"), "xsd:dateTime"]
+
+ print(infer(["Mon Jan 13 09:52:52 MST 2014"]))
print(infer([data1[0]]))
print()
- print(infer(['2022-01-15T08:00:00']))
+ print(infer(["2022-01-15T08:00:00"]))
print(infer([data1[0]]))
print()
@@ -149,7 +189,7 @@ def stob(value):
try:
print(dataConversionType.convert(data4[0], data4[2]))
except Exception:
- print('Exception')
+ print("Exception")
# Convert datetime generated into UTC format: 2021-12-21T16:18:55Z or 2021-12-21T16:18:55+00:00, ISO8601
@@ -158,3 +198,6 @@ def stob(value):
print(dataConversionType.convert(data6[0], data6[2]))
print(dataConversionType.convert(data7[0], data7[2]))
+
+ data101 = ['"3016.9"', Token("FORMATCONNECTOR", "^^"), "xsd:float"]
+ print(dataConversionType.convert(data101[0], data101[2]))
diff --git a/sdmx2jsonld/common/listmanagement.py b/sdmx2jsonld/common/listmanagement.py
index eeae59f..b07a0f1 100644
--- a/sdmx2jsonld/common/listmanagement.py
+++ b/sdmx2jsonld/common/listmanagement.py
@@ -20,6 +20,7 @@
# under the License.
##
from logging import getLogger
+from sdmx2jsonld.exceptions.exceptions import ClassExtractPrefixError
logger = getLogger()
@@ -36,12 +37,12 @@ def filter_key_with_prefix(prefix_key, not_allowed_keys, further_process_keys):
# this is a key with prefix that we want to keep
return True
else:
- if aux[1] not in ['component', 'label']:
+ if aux[1] not in ["component", "label"]:
# These are the identified not allowed keys, we need to inform about them
- logger.warn(f'The property {aux[1]} is not supported in statDCAT-AP')
+ logger.warning(f"The property {aux[1]} is not supported in statDCAT-AP")
else:
# These are the identified keys managed in a different way
- logger.info(f'The property {aux[1]} is manage afterwards in Dataset Class or in Property Class')
+ logger.info(f"The property {aux[1]} is manage afterwards in Dataset Class or in Property Class")
return False
else:
@@ -50,18 +51,82 @@ def filter_key_with_prefix(prefix_key, not_allowed_keys, further_process_keys):
def flatten_value(y):
if isinstance(y, list):
- return flatten_value(y[0])
+ aux = len(y)
+ if aux == 1:
+ return flatten_value(y[0])
+ elif aux > 1:
+ # for each element of the list we have to flatten to string and create the corresponding list
+ # We need to differentiate between multilingual case and multiple data
+ # this case corresponds to multilingual content
+ if len(y[0]) == 2:
+ # Multilingual case
+ result = dict()
+ for i in range(0, aux):
+ result[y[i][1][1:]] = flatten_value(y[i][0])
+ else:
+ # Normal case
+ result = list()
+ for i in range(0, aux):
+ result.append(flatten_value(y[i]))
+ return result
+ else: # in case of len == 0 be return the empty string
+ return ""
else:
- return y.replace('"', '')
+ return y.replace('"', "")
-def get_rest_data(data, not_allowed_keys, further_process_keys):
+def get_rest_data(data, not_allowed_keys=None, further_process_keys=None):
+ if further_process_keys is None:
+ further_process_keys = []
+ if not_allowed_keys is None:
+ not_allowed_keys = []
aux = {data[i]: flatten_value(data[i + 1]) for i in range(0, len(data), 2)}
# We need to get the list of keys from the dict
new_keys = list(
- filter(lambda x: filter_key_with_prefix(x, not_allowed_keys, further_process_keys), list(aux.keys())))
+ filter(
+ lambda x: filter_key_with_prefix(x, not_allowed_keys, further_process_keys),
+ list(aux.keys()),
+ )
+ )
new_data = {k: aux[k] for k in new_keys}
+ corrected_dict = {k.replace(k, extract_prefix(k)): v for k, v in new_data.items()}
+
+ return corrected_dict
+
- return new_data
+def extract_prefix(attribute):
+ result = None
+ if attribute is None or len(attribute) == 0:
+ raise ClassExtractPrefixError(data=attribute, message=f"Unexpected data received: '{attribute}'")
+ else:
+ data = attribute.split(":")
+
+ if len(data) == 1:
+ result = data[0]
+ elif len(data) == 2:
+ result = data[1]
+ else:
+ raise ClassExtractPrefixError(data=attribute, message=f"Unexpected number of prefixes: '{attribute}'")
+
+ return result
+
+
+def get_property_value(data, property_name):
+ # At the moment, we only find the first occurs of the property
+ i = 0
+ key = ""
+ found = -1
+ for i in range(0, len(data)):
+ key = data[i]
+ if isinstance(key, str):
+ found = key.find(property_name)
+ if found != -1:
+ # We found the key
+ break
+
+ if found != -1:
+ return i, key, data[i + 1]
+ else:
+ return -1, "", ""
diff --git a/sdmx2jsonld/common/rdf.py b/sdmx2jsonld/common/rdf.py
index 9e7808b..65daa3c 100644
--- a/sdmx2jsonld/common/rdf.py
+++ b/sdmx2jsonld/common/rdf.py
@@ -23,6 +23,10 @@
def turtle_terse(rdf_content):
+ """
+ Function which receives a string in formatted with RDF, dialect turtle, parses it and returns the same data
+ parsed in another string. The incoming data in the parameter is equivalent to the returned data.
+ """
# Create a Graph
g2 = Graph()
diff --git a/sdmx2jsonld/common/regparser.py b/sdmx2jsonld/common/regparser.py
index 39e8061..bcdfdb6 100644
--- a/sdmx2jsonld/common/regparser.py
+++ b/sdmx2jsonld/common/regparser.py
@@ -25,24 +25,28 @@
class RegParser:
def __init__(self):
- regex = "http[s]?:\/\/(.*)"
+ regex = "http[s]?:\/\/(.+)"
# Compile the Regex
self.re = re.compile(regex)
- def obtain_id(self, string_to_parse):
+ def obtain_id(self, string_to_parse, prefix_string=""):
# Return if the string matched the ReGex
out = self.re.match(string_to_parse)
if out is None:
# Check if the prefixed name include ':'
- obtained_id = string_to_parse.split(':')[1]
+ try:
+ obtained_id = string_to_parse.split(":")[1]
+ except IndexError:
+ # We have a normal prefix or data
+ obtained_id = string_to_parse
else:
# We have a URIREF
out = out.group(1)
out = out.split("/")
# we get the last value which corresponds to the id
- obtained_id = out[(len(out) - 1):][0]
+ obtained_id = prefix_string + out[(len(out) - 1) :][0]
return obtained_id
diff --git a/sdmx2jsonld/common/tzinfos.py b/sdmx2jsonld/common/tzinfos.py
new file mode 100644
index 0000000..4e41b22
--- /dev/null
+++ b/sdmx2jsonld/common/tzinfos.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+whois_timezone_info = {
+ "A": 1 * 3600,
+ "ACDT": 10.5 * 3600,
+ "ACST": 9.5 * 3600,
+ "ACT": -5 * 3600,
+ "ACWST": 8.75 * 3600,
+ "ADT": 4 * 3600,
+ "AEDT": 11 * 3600,
+ "AEST": 10 * 3600,
+ "AET": 10 * 3600,
+ "AFT": 4.5 * 3600,
+ "AKDT": -8 * 3600,
+ "AKST": -9 * 3600,
+ "ALMT": 6 * 3600,
+ "AMST": -3 * 3600,
+ "AMT": -4 * 3600,
+ "ANAST": 12 * 3600,
+ "ANAT": 12 * 3600,
+ "AQTT": 5 * 3600,
+ "ART": -3 * 3600,
+ "AST": 3 * 3600,
+ "AT": -4 * 3600,
+ "AWDT": 9 * 3600,
+ "AWST": 8 * 3600,
+ "AZOST": 0 * 3600,
+ "AZOT": -1 * 3600,
+ "AZST": 5 * 3600,
+ "AZT": 4 * 3600,
+ "AoE": -12 * 3600,
+ "B": 2 * 3600,
+ "BNT": 8 * 3600,
+ "BOT": -4 * 3600,
+ "BRST": -2 * 3600,
+ "BRT": -3 * 3600,
+ "BST": 6 * 3600,
+ "BTT": 6 * 3600,
+ "C": 3 * 3600,
+ "CAST": 8 * 3600,
+ "CAT": 2 * 3600,
+ "CCT": 6.5 * 3600,
+ "CDT": -5 * 3600,
+ "CEST": 2 * 3600,
+ "CET": 1 * 3600,
+ "CHADT": 13.75 * 3600,
+ "CHAST": 12.75 * 3600,
+ "CHOST": 9 * 3600,
+ "CHOT": 8 * 3600,
+ "CHUT": 10 * 3600,
+ "CIDST": -4 * 3600,
+ "CIST": -5 * 3600,
+ "CKT": -10 * 3600,
+ "CLST": -3 * 3600,
+ "CLT": -4 * 3600,
+ "COT": -5 * 3600,
+ "CST": -6 * 3600,
+ "CT": -6 * 3600,
+ "CVT": -1 * 3600,
+ "CXT": 7 * 3600,
+ "ChST": 10 * 3600,
+ "D": 4 * 3600,
+ "DAVT": 7 * 3600,
+ "DDUT": 10 * 3600,
+ "E": 5 * 3600,
+ "EASST": -5 * 3600,
+ "EAST": -6 * 3600,
+ "EAT": 3 * 3600,
+ "ECT": -5 * 3600,
+ "EDT": -4 * 3600,
+ "EEST": 3 * 3600,
+ "EET": 2 * 3600,
+ "EGST": 0 * 3600,
+ "EGT": -1 * 3600,
+ "EST": -5 * 3600,
+ "ET": -5 * 3600,
+ "F": 6 * 3600,
+ "FET": 3 * 3600,
+ "FJST": 13 * 3600,
+ "FJT": 12 * 3600,
+ "FKST": -3 * 3600,
+ "FKT": -4 * 3600,
+ "FNT": -2 * 3600,
+ "G": 7 * 3600,
+ "GALT": -6 * 3600,
+ "GAMT": -9 * 3600,
+ "GET": 4 * 3600,
+ "GFT": -3 * 3600,
+ "GILT": 12 * 3600,
+ "GMT": 0 * 3600,
+ "GST": 4 * 3600,
+ "GYT": -4 * 3600,
+ "H": 8 * 3600,
+ "HDT": -9 * 3600,
+ "HKT": 8 * 3600,
+ "HOVST": 8 * 3600,
+ "HOVT": 7 * 3600,
+ "HST": -10 * 3600,
+ "I": 9 * 3600,
+ "ICT": 7 * 3600,
+ "IDT": 3 * 3600,
+ "IOT": 6 * 3600,
+ "IRDT": 4.5 * 3600,
+ "IRKST": 9 * 3600,
+ "IRKT": 8 * 3600,
+ "IRST": 3.5 * 3600,
+ "IST": 5.5 * 3600,
+ "JST": 9 * 3600,
+ "K": 10 * 3600,
+ "KGT": 6 * 3600,
+ "KOST": 11 * 3600,
+ "KRAST": 8 * 3600,
+ "KRAT": 7 * 3600,
+ "KST": 9 * 3600,
+ "KUYT": 4 * 3600,
+ "L": 11 * 3600,
+ "LHDT": 11 * 3600,
+ "LHST": 10.5 * 3600,
+ "LINT": 14 * 3600,
+ "M": 12 * 3600,
+ "MAGST": 12 * 3600,
+ "MAGT": 11 * 3600,
+ "MART": 9.5 * 3600,
+ "MAWT": 5 * 3600,
+ "MDT": -6 * 3600,
+ "MHT": 12 * 3600,
+ "MMT": 6.5 * 3600,
+ "MSD": 4 * 3600,
+ "MSK": 3 * 3600,
+ "MST": -7 * 3600,
+ "MT": -7 * 3600,
+ "MUT": 4 * 3600,
+ "MVT": 5 * 3600,
+ "MYT": 8 * 3600,
+ "N": -1 * 3600,
+ "NCT": 11 * 3600,
+ "NDT": 2.5 * 3600,
+ "NFT": 11 * 3600,
+ "NOVST": 7 * 3600,
+ "NOVT": 7 * 3600,
+ "NPT": 5.5 * 3600,
+ "NRT": 12 * 3600,
+ "NST": 3.5 * 3600,
+ "NUT": -11 * 3600,
+ "NZDT": 13 * 3600,
+ "NZST": 12 * 3600,
+ "O": -2 * 3600,
+ "OMSST": 7 * 3600,
+ "OMST": 6 * 3600,
+ "ORAT": 5 * 3600,
+ "P": -3 * 3600,
+ "PDT": -7 * 3600,
+ "PET": -5 * 3600,
+ "PETST": 12 * 3600,
+ "PETT": 12 * 3600,
+ "PGT": 10 * 3600,
+ "PHOT": 13 * 3600,
+ "PHT": 8 * 3600,
+ "PKT": 5 * 3600,
+ "PMDT": -2 * 3600,
+ "PMST": -3 * 3600,
+ "PONT": 11 * 3600,
+ "PST": -8 * 3600,
+ "PT": -8 * 3600,
+ "PWT": 9 * 3600,
+ "PYST": -3 * 3600,
+ "PYT": -4 * 3600,
+ "Q": -4 * 3600,
+ "QYZT": 6 * 3600,
+ "R": -5 * 3600,
+ "RET": 4 * 3600,
+ "ROTT": -3 * 3600,
+ "S": -6 * 3600,
+ "SAKT": 11 * 3600,
+ "SAMT": 4 * 3600,
+ "SAST": 2 * 3600,
+ "SBT": 11 * 3600,
+ "SCT": 4 * 3600,
+ "SGT": 8 * 3600,
+ "SRET": 11 * 3600,
+ "SRT": -3 * 3600,
+ "SST": -11 * 3600,
+ "SYOT": 3 * 3600,
+ "T": -7 * 3600,
+ "TAHT": -10 * 3600,
+ "TFT": 5 * 3600,
+ "TJT": 5 * 3600,
+ "TKT": 13 * 3600,
+ "TLT": 9 * 3600,
+ "TMT": 5 * 3600,
+ "TOST": 14 * 3600,
+ "TOT": 13 * 3600,
+ "TRT": 3 * 3600,
+ "TVT": 12 * 3600,
+ "U": -8 * 3600,
+ "ULAST": 9 * 3600,
+ "ULAT": 8 * 3600,
+ "UTC": 0 * 3600,
+ "UYST": -2 * 3600,
+ "UYT": -3 * 3600,
+ "UZT": 5 * 3600,
+ "V": -9 * 3600,
+ "VET": -4 * 3600,
+ "VLAST": 11 * 3600,
+ "VLAT": 10 * 3600,
+ "VOST": 6 * 3600,
+ "VUT": 11 * 3600,
+ "W": -10 * 3600,
+ "WAKT": 12 * 3600,
+ "WARST": -3 * 3600,
+ "WAST": 2 * 3600,
+ "WAT": 1 * 3600,
+ "WEST": 1 * 3600,
+ "WET": 0 * 3600,
+ "WFT": 12 * 3600,
+ "WGST": -2 * 3600,
+ "WGT": -3 * 3600,
+ "WIB": 7 * 3600,
+ "WIT": 9 * 3600,
+ "WITA": 8 * 3600,
+ "WST": 14 * 3600,
+ "WT": 0 * 3600,
+ "X": -11 * 3600,
+ "Y": -12 * 3600,
+ "YAKST": 10 * 3600,
+ "YAKT": 9 * 3600,
+ "YAPT": 10 * 3600,
+ "YEKST": 6 * 3600,
+ "YEKT": 5 * 3600,
+ "Z": 0 * 3600,
+}
diff --git a/sdmx2jsonld/cube/__init__.py b/sdmx2jsonld/cube/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdmx2jsonld/cube/measuretype.py b/sdmx2jsonld/cube/measuretype.py
new file mode 100644
index 0000000..3aee669
--- /dev/null
+++ b/sdmx2jsonld/cube/measuretype.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassFreqError
+from sdmx2jsonld.sdmxdimensions.sdmxdimension import SDMXDimension
+
+
+class MeasureType(SDMXDimension):
+ def __init__(self):
+ # qb:measureType a qb:DimensionProperty, rdf:Property;
+ # rdfs:label "measure type"@en;
+ # rdfs:comment "Generic measure dimension, the value of this dimension indicates which measure (from the set of measures in the DSD) is being given by the obsValue (or other primary measure)"@en;
+ # rdfs:range qb:MeasureProperty;
+ # rdfs:isDefinedBy ;
+ super().__init__(
+ entity_id="measureType",
+ label="measure type",
+ description="Generic measure dimension, the value of this dimension indicates which measure "
+ "(from the set of measures in the DSD) is being given by the obsValue "
+ "(or other primary measure).",
+ concept_id=None,
+ identifier="measureType",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/exceptions/__init__.py b/sdmx2jsonld/exceptions/__init__.py
index 575252e..8feabd6 100644
--- a/sdmx2jsonld/exceptions/__init__.py
+++ b/sdmx2jsonld/exceptions/__init__.py
@@ -26,9 +26,7 @@
class UnexpectedEOF(LarkUnexpectedEOF):
def __init__(self, expected, state=None, terminals_by_name=None):
- super(LarkUnexpectedEOF, self).__init__(expected=expected,
- state=state,
- terminals_by_name=terminals_by_name)
+ super(LarkUnexpectedEOF, self).__init__(expected=expected, state=state, terminals_by_name=terminals_by_name)
class UnexpectedInput(LarkUnexpectedInput):
@@ -37,11 +35,22 @@ def __init__(self):
class UnexpectedToken(LarkUnexpectedToken):
- def __init__(self, token, expected, considered_rules=None, state=None, interactive_parser=None, terminals_by_name=None, token_history=None):
- super(LarkUnexpectedToken, self).__init__(token=token,
- expected=expected,
- considered_rules=considered_rules,
- state=state,
- interactive_parser=interactive_parser,
- terminals_by_name=terminals_by_name,
- token_history=token_history)
+ def __init__(
+ self,
+ token,
+ expected,
+ considered_rules=None,
+ state=None,
+ interactive_parser=None,
+ terminals_by_name=None,
+ token_history=None,
+ ):
+ super(LarkUnexpectedToken, self).__init__(
+ token=token,
+ expected=expected,
+ considered_rules=considered_rules,
+ state=state,
+ interactive_parser=interactive_parser,
+ terminals_by_name=terminals_by_name,
+ token_history=token_history,
+ )
diff --git a/sdmx2jsonld/exceptions/exceptions.py b/sdmx2jsonld/exceptions/exceptions.py
new file mode 100644
index 0000000..509d3ca
--- /dev/null
+++ b/sdmx2jsonld/exceptions/exceptions.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+class ClassSDMXAttributeError(Exception):
+ """Base class for other exceptions"""
+
+ def __init__(self, data, message):
+ self.message = message
+ self.data = data
+
+ def __str__(self):
+ return f"{self.data} -> {self.message}"
+
+
+class ClassConfStatusError(ClassSDMXAttributeError):
+ """Raised when the input value is not included in the list of available values for confStatus"""
+
+ """Exception raised for errors in the input data.
+
+ Attributes:
+ data -- input data which caused the error
+ message -- explanation of the error
+ """
+
+ def __init__(self, data, message="ConfStatus value is not the expected"):
+ super().__init__(data=data, message=message)
+
+
+class ClassObsStatusError(ClassSDMXAttributeError):
+ """Raised when the input value is not included in the list of available values for obsStatus"""
+
+ """Exception raised for errors in the input data.
+
+ Attributes:
+ data -- input data which caused the error
+ message -- explanation of the error
+ """
+
+ def __init__(self, data, message="ObsStatus value is not the expected"):
+ super().__init__(data=data, message=message)
+
+
+class ClassCode(ClassSDMXAttributeError):
+ """Raised when the input value is not included in the list of available values for unitMult and decimals"""
+
+ """Exception raised for errors in the input data.
+
+ Attributes:
+ data -- input data which caused the error
+ message -- explanation of the error
+ """
+
+ def __init__(self, data, message="Decimals value is not the expected"):
+ super().__init__(data=data, message=message)
+
+
+class ClassFreqError(ClassSDMXAttributeError):
+ """Raised when the input value is not included in the list of available values for Freq"""
+
+ """Exception raised for errors in the input data.
+
+ Attributes:
+ data -- input data which caused the error
+ message -- explanation of the error
+ """
+
+ def __init__(self, data, message="Decimals value is not the expected"):
+ super().__init__(data=data, message=message)
+
+
+class ClassExtractPrefixError(ClassSDMXAttributeError):
+ """Raised when the input value is None or Empty or includes several prefixes"""
+
+ """Exception raised for errors in the input data.
+
+ Attributes:
+ data -- input data which caused the error
+ message -- explanation of the error
+ """
+
+ def __init__(self, data, message="Value is not the expected"):
+ super().__init__(data=data, message=message)
diff --git a/sdmx2jsonld/sdmxattributes/__init__.py b/sdmx2jsonld/sdmxattributes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdmx2jsonld/sdmxattributes/code.py b/sdmx2jsonld/sdmxattributes/code.py
new file mode 100644
index 0000000..1b340ea
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/code.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassCode
+
+
+class Code:
+ status: list
+ type: str
+ data_range: range
+
+ def __init__(self, typecode):
+ self.typecode = typecode
+
+ if typecode == "decimals":
+ self.data_range = range(0, 15)
+ elif typecode == "unitMult":
+ self.data_range = range(0, 13)
+
+ def fix_value(self, value):
+ # Need to check if the value received is in the list of possible values -> return that value
+ # then maybe could be in the form decimals- or unitMult-, so we have to extract
+ # the substring and return that substring if it is in the list of values, if not return an error.
+ # any other value will return an error
+ number: int() = 0
+
+ m = search(f"sdmx-code:{self.typecode}-(.*)", str(value))
+
+ if m is not None:
+ number = int(m.group(1))
+ else:
+ # The data is not following the sdmx-code: we have to check which one
+ # 1) Check if there is a value without the prefix
+ m = search(f"{self.typecode}-(.*)", str(value))
+
+ if m is not None:
+ number = int(m.group(1))
+ else:
+ # We need to check is there is an integer number between a valid range
+ if isinstance(value, int):
+ # Need to check the range
+ number = value
+ elif isinstance(value, str):
+ try:
+ number = int(value)
+ except ValueError:
+ raise ClassCode(data=value, message=f"Data is not a valid value")
+
+ if number not in self.data_range:
+ raise ClassCode(
+ data=value,
+ message=f"{self.typecode} out of range, got: {number} {self.data_range}",
+ )
+
+ return number
diff --git a/sdmx2jsonld/sdmxattributes/compilingorg.py b/sdmx2jsonld/sdmxattributes/compilingorg.py
new file mode 100644
index 0000000..45434e0
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/compilingorg.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class CompilingOrg(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:compilingOrg a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:compilingOrg ;
+ # rdfs:label "Compiling agency"@en ;
+ # rdfs:comment """The organisation compiling the data being reported."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="compilingOrg",
+ label="Compiling agency",
+ description="The organisation compiling the data being reported.",
+ concept_id="compilingOrg",
+ identifier="compilingOrg",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/confirmationStatus.py b/sdmx2jsonld/sdmxattributes/confirmationStatus.py
new file mode 100644
index 0000000..1b60374
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/confirmationStatus.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassConfStatusError
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class ConfStatus(SDMXAttribute):
+ status: list = ["F", "N", "C", "D", "S", "A", "O", "T", "G", "M", "E", "P"]
+
+ def __init__(self):
+ # sdmx-attribute:confStatus a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:confStatus ;
+ # rdfs:label "Confidentiality - status"@en ;
+ # rdfs:comment """Information about the confidentiality status of the object to which this
+ # attribute is attached."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="confStatus",
+ label="Confidentiality - status",
+ description="Information about the confidentiality status of the object "
+ "to which this attribute is attached.",
+ concept_id="confStatus",
+ identifier="confStatus",
+ entity_range="xsd:string",
+ )
+
+ def fix_value(self, value):
+ # Need to check if the value received is in the list of possible values -> return that value
+ # then maybe could be in the form confStatus-, so we have to extract the substring and
+ # return that substring if it is in the list of values, if not return an error.
+ # any other value will return an error
+ value_upper = value.upper()
+
+ if value_upper in self.status:
+ return value_upper
+ else:
+ # we could receive a value in the format confStatus-
+ m = search("CONFSTATUS-(.*)", value_upper)
+
+ if m is not None:
+ status = m.group(1)
+
+ if status in self.status:
+ return status
+ else:
+ message = (
+ f"ConfStatus value is not included in the list of available values,\n"
+ f" got:{value}\n"
+ f" expected:{['confStatus-'+x for x in self.status]}"
+ )
+
+ raise ClassConfStatusError(data=value, message=message)
+
+ else:
+ # We received a value that it is not following the template format
+ raise ClassConfStatusError(value)
diff --git a/sdmx2jsonld/sdmxattributes/currency.py b/sdmx2jsonld/sdmxattributes/currency.py
new file mode 100644
index 0000000..a657e6e
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/currency.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class Currency(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:currency a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:currency ;
+ # rdfs:label "Currency"@en ;
+ # rdfs:comment """Monetary denomination of the object being measured."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="currency",
+ label="Currency",
+ description="Monetary denomination of the object being measured.",
+ concept_id="currency",
+ identifier="currency",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/dataComp.py b/sdmx2jsonld/sdmxattributes/dataComp.py
new file mode 100644
index 0000000..37fa698
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/dataComp.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class DataComp(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:dataComp a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:dataComp ;
+ # rdfs:label "Data Compilation"@en ;
+ # rdfs:comment """Operations performed on data to derive new information according
+ # to a given set of rules."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="dataComp",
+ label="Data Compilation",
+ description="Operations performed on data to derive new information according to a " "given set of rules.",
+ concept_id="dataComp",
+ identifier="dataComp",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/decimals.py b/sdmx2jsonld/sdmxattributes/decimals.py
new file mode 100644
index 0000000..716f4bf
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/decimals.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class Decimals(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:decimals a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:decimals ;
+ # rdfs:label "Decimals"@en ;
+ # rdfs:comment """The number of digits of an observation to the right of a decimal point."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="decimals",
+ label="Decimals",
+ description="The number of digits of an observation to the right of a decimal point.",
+ concept_id="decimals",
+ identifier="decimals",
+ entity_range="xsd:integer",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/dissorg.py b/sdmx2jsonld/sdmxattributes/dissorg.py
new file mode 100644
index 0000000..01b57eb
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/dissorg.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class DissOrg(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:dissOrg a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:dissOrg ;
+ # rdfs:label "Data Dissemination Agency"@en ;
+ # rdfs:comment """The organisation disseminating the data."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="dissOrg",
+ label="Data Dissemination Agency",
+ description="The organisation disseminating the data.",
+ concept_id="dissOrg",
+ identifier="dissOrg",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/observationStatus.py b/sdmx2jsonld/sdmxattributes/observationStatus.py
new file mode 100644
index 0000000..e5b7f9a
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/observationStatus.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassObsStatusError
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class ObsStatus(SDMXAttribute):
+ status: list = [
+ "A",
+ "B",
+ "D",
+ "E",
+ "F",
+ "G",
+ "I",
+ "K",
+ "W",
+ "O",
+ "M",
+ "P",
+ "S",
+ "L",
+ "H",
+ "Q",
+ "J",
+ "N",
+ "U",
+ "V",
+ ]
+
+ def __init__(self):
+ # sdmx-attribute:obsStatus a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:obsStatus ;
+ # rdfs:label "Observation Status"@en ;
+ # rdfs:comment """Information on the quality of a value or an unusual or missing value."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="obsStatus",
+ label="Observation Status",
+ description="Information on the quality of a value or an unusual or missing value.",
+ concept_id="obsStatus",
+ identifier="obsStatus",
+ entity_range="xsd:string",
+ )
+
+ def fix_value(self, value):
+ # Need to check if the value received is in the list of possible values -> return that value
+ # then maybe could be in the form obsStatus-, so we have to extract the substring and
+ # return that substring if it is in the list of values, if not return an error.
+ # any other value will return an error
+ value_upper = value.upper()
+
+ if value_upper in self.status:
+ return value_upper
+ else:
+ # we could receive a value in the format obsStatus-
+ m = search("OBSSTATUS-(.*)", value_upper)
+
+ if m is not None:
+ status = m.group(1)
+
+ if status in self.status:
+ return status
+ else:
+ message = (
+ f"ObsStatus value is not included in the list of available values,\n"
+ f" got:{value}\n"
+ f" expected:{['obsStatus-'+x for x in self.status]}"
+ )
+
+ raise ClassObsStatusError(data=value, message=message)
+
+ else:
+ # We received a value that it is not following the template format
+ raise ClassObsStatusError(value)
diff --git a/sdmx2jsonld/sdmxattributes/sdmxattribute.py b/sdmx2jsonld/sdmxattributes/sdmxattribute.py
new file mode 100644
index 0000000..e4567bf
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/sdmxattribute.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.common.commonclass import CommonClass
+
+
+class SDMXAttribute(CommonClass):
+ def __init__(self, entity_id, label, description, concept_id, identifier, entity_range):
+ super().__init__(entity="AttributeProperty")
+ self.data = {
+ "id": f"urn:ngsi-ld:AttributeProperty:{entity_id}",
+ "type": "AttributeProperty",
+ "language": {"type": "Property", "value": ["en"]},
+ "label": {
+ "type": "Property",
+ "value": {
+ "en": label,
+ },
+ },
+ "description": {
+ "type": "Property",
+ "value": {
+ "en": description,
+ },
+ },
+ "concept": {
+ "type": "Relationship",
+ "object": f"urn:ngsi-ld:Concept:{concept_id}",
+ },
+ "identifier": {"type": "Property", "value": identifier},
+ "range": {"type": "Property", "value": entity_range},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
+ }
diff --git a/sdmx2jsonld/sdmxattributes/timeFormat.py b/sdmx2jsonld/sdmxattributes/timeFormat.py
new file mode 100644
index 0000000..3db8273
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/timeFormat.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class TimeFormat(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:timeFormat a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:timeFormat ;
+ # rdfs:label "Time Format"@en ;
+ # rdfs:comment """Technical format in which time is represented for the measured phenomenon."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="timeFormat",
+ label="Time Format",
+ description="Technical format in which time is represented for the measured phenomenon.",
+ concept_id="timeFormat",
+ identifier="timeFormat",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/timePerCollect.py b/sdmx2jsonld/sdmxattributes/timePerCollect.py
new file mode 100644
index 0000000..f219ec0
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/timePerCollect.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class TimePerCollect(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:timePerCollect a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:timePerCollect ;
+ # rdfs:label "Time Period - collection"@en ;
+ # rdfs:comment """Dates or periods during which the observations have been collected
+ # (such as middle, average or end of period) to compile the indicator
+ # for the target reference period."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="timePerCollect",
+ label="Time Period - collection",
+ description="Dates or periods during which the observations have been collected "
+ "(such as middle, average or end of period) to compile the indicator "
+ "for the target reference period.",
+ concept_id="timePerCollect",
+ identifier="timePerCollect",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/title.py b/sdmx2jsonld/sdmxattributes/title.py
new file mode 100644
index 0000000..e8e257a
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/title.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class Title(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:title a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:title ;
+ # rdfs:label "Title"@en ;
+ # rdfs:comment """Textual label used as identification of a statistical object."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="title",
+ label="Title",
+ description="Textual label used as identification of a statistical object.",
+ concept_id="title",
+ identifier="title",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxattributes/unitmult.py b/sdmx2jsonld/sdmxattributes/unitmult.py
new file mode 100644
index 0000000..6ddc910
--- /dev/null
+++ b/sdmx2jsonld/sdmxattributes/unitmult.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxattributes.sdmxattribute import SDMXAttribute
+
+
+class UnitMult(SDMXAttribute):
+ def __init__(self):
+ # sdmx-attribute:unitMult a qb:AttributeProperty, rdf:Property ;
+ # qb:concept sdmx-concept:unitMult ;
+ # rdfs:label "Unit Multiplier"@en ;
+ # rdfs:comment """Exponent in base 10 specified so that multiplying the observation
+ # numeric values by 10^UNIT_MULT gives a value expressed in the UNIT."""@en ;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="unitMult",
+ label="Unit Multiplier",
+ description="Exponent in base 10 specified so that multiplying the observation numeric "
+ "values by 10^UNIT_MULT gives a value expressed in the UNIT.",
+ concept_id="unitMult",
+ identifier="unitMult",
+ entity_range="xsd:integer",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/__init__.py b/sdmx2jsonld/sdmxconcepts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdmx2jsonld/sdmxconcepts/cogconceptschema.py b/sdmx2jsonld/sdmxconcepts/cogconceptschema.py
new file mode 100644
index 0000000..96d8e94
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/cogconceptschema.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.common.commonclass import CommonClass
+
+
+class CogConceptSchema(CommonClass):
+ def __init__(self):
+ # sdmx-concept:cog a skos:ConceptScheme;
+ # rdfs:label "Content Oriented Guidelines concept scheme"@en;
+ # rdfs:isDefinedBy .
+ super().__init__(entity="ConceptSchema")
+
+ self.data = {
+ "id": "urn:ngsi-ld:ConceptSchema:cog",
+ "type": "ConceptScheme",
+ "language": {"type": "Property", "value": ["en"]},
+ "prefLabel": {
+ "type": "Property",
+ "value": {"en": "Content Oriented Guidelines concept scheme"},
+ },
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
+ }
diff --git a/sdmx2jsonld/sdmxconcepts/compilingorgconcept.py b/sdmx2jsonld/sdmxconcepts/compilingorgconcept.py
new file mode 100644
index 0000000..90275bb
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/compilingorgconcept.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class CompilingOrgConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:compilingOrg a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Compiling agency"@en ;
+ # rdfs:comment """The organisation compiling the data being reported."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].COMPILING_ORG";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="compilingOrg",
+ label="Compiling agency",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept="
+ "SDMX:CROSS_DOMAIN_CONCEPTS[1.0].COMPILING_ORG",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/confstatusconcept.py b/sdmx2jsonld/sdmxconcepts/confstatusconcept.py
new file mode 100644
index 0000000..3d3254c
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/confstatusconcept.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class ConfStatusConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:confStatus a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Confidentiality - status"@en ;
+ # rdfs:comment """Information about the confidentiality status of the object to which this
+ # attribute is attached."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].CONF_STATUS";
+ # skos:broader sdmx-concept:conf;
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="confStatus",
+ label="Confidentiality - status",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].CONF_STATUS",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/currencyconcept.py b/sdmx2jsonld/sdmxconcepts/currencyconcept.py
new file mode 100644
index 0000000..d4543ab
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/currencyconcept.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class CurrencyConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:currency a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Currency"@en ;
+ # rdfs:comment """Monetary denomination of the object being measured."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].CURRENCY";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="currency",
+ label="Currency",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].CURRENCY",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/datacompconcept.py b/sdmx2jsonld/sdmxconcepts/datacompconcept.py
new file mode 100644
index 0000000..71f0bd1
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/datacompconcept.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class DataCompConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:dataComp a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Data Compilation"@en ;
+ # rdfs:comment """Operations performed on data to derive new information according
+ # to a given set of rules."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].DATA_COMP";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="dataComp",
+ label="Data Compilation",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=" "SDMX:CROSS_DOMAIN_CONCEPTS[1.0].DATA_COMP",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/decimals.py b/sdmx2jsonld/sdmxconcepts/decimals.py
new file mode 100644
index 0000000..c0eec8c
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/decimals.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class DecimalsConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:decimals a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Decimals"@en ;
+ # rdfs:comment """The number of digits of an observation to the right of a decimal point."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:
+ # CROSS_DOMAIN_CONCEPTS[1.0].DECIMALS";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="decimals",
+ label="Decimals",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].DECIMALS",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/dissorgconcept.py b/sdmx2jsonld/sdmxconcepts/dissorgconcept.py
new file mode 100644
index 0000000..53bd28b
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/dissorgconcept.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class DissOrgConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:dissOrg a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Data Dissemination Agency"@en ;
+ # rdfs:comment """The organisation disseminating the data."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:
+ # CROSS_DOMAIN_CONCEPTS[1.0].DISS_ORG";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="dissOrg",
+ label="Data Dissemination Agency",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].DISS_ORG",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/freqconcept.py b/sdmx2jsonld/sdmxconcepts/freqconcept.py
new file mode 100644
index 0000000..59a071c
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/freqconcept.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class FreqConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:freq a sdmx:Concept, skos:Concept;
+ # rdfs:label "Frequency"@en;
+ # rdfs:comment """The time interval at which observations occur over a given time period."""@en;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].FREQ";
+ # skos:inScheme sdmx-concept:cog.
+ super().__init__(
+ entity_id="freq",
+ label="Frequency",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].FREQ",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/obsstatusconcept.py b/sdmx2jsonld/sdmxconcepts/obsstatusconcept.py
new file mode 100644
index 0000000..8c753ca
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/obsstatusconcept.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class ObsStatusConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:obsStatus a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Observation Status"@en ;
+ # rdfs:comment """Information on the quality of a value or an unusual or missing value."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:
+ # CROSS_DOMAIN_CONCEPTS[1.0].OBS_STATUS";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="obsStatus",
+ label="Observation Status",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].OBS_STATUS",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/refareaconcept.py b/sdmx2jsonld/sdmxconcepts/refareaconcept.py
new file mode 100644
index 0000000..2a16c2f
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/refareaconcept.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class RefAreaConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:refArea a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Reference Area"@en ;
+ # rdfs:comment """The country or geographic area to which the measured statistical phenomenon relates."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].REF_AREA";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="refArea",
+ label="Reference Area",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].REF_AREA",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/sdmxconcept.py b/sdmx2jsonld/sdmxconcepts/sdmxconcept.py
new file mode 100644
index 0000000..fdbbf16
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/sdmxconcept.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.common.commonclass import CommonClass
+
+
+class SDMXConcept(CommonClass):
+ def __init__(self, entity_id, label, notation):
+ super().__init__(entity="Concept")
+ self.data = {
+ "id": f"urn:ngsi-ld:Concept:{entity_id}",
+ "type": "Concept",
+ "language": {"type": "Property", "value": ["en"]},
+ "inScheme": {
+ "type": "Relationship",
+ "object": "urn:ngsi-ld:ConceptSchema:cog",
+ },
+ "prefLabel": {"type": "Property", "value": {"en": label}},
+ "notation": {"type": "Property", "value": notation},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
+ }
diff --git a/sdmx2jsonld/sdmxconcepts/timePerCollectConcept.py b/sdmx2jsonld/sdmxconcepts/timePerCollectConcept.py
new file mode 100644
index 0000000..20a6867
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/timePerCollectConcept.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class TimePerCollectConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:timePerCollect a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Time Period - collection"@en ;
+ # rdfs:comment """Dates or periods during which the observations have been collected
+ # (such as middle, average or end of period) to compile the indicator
+ # for the target reference period."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].TIME_PER_COLLECT";
+ # skos:broader sdmx-concept:timePeriod;
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="timePerCollect",
+ label="Time Period - collection",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:"
+ "CROSS_DOMAIN_CONCEPTS[1.0].TIME_PER_COLLECT",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/timeformatconcept.py b/sdmx2jsonld/sdmxconcepts/timeformatconcept.py
new file mode 100644
index 0000000..72d2f08
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/timeformatconcept.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class TimeFormatConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:timeFormat a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Time Format"@en ;
+ # rdfs:comment """Technical format in which time is represented for the measured phenomenon."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:
+ # CROSS_DOMAIN_CONCEPTS[1.0].TIME_FORMAT";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="timeFormat",
+ label="Time Format",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].TIME_FORMAT",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/timeperiodconcept.py b/sdmx2jsonld/sdmxconcepts/timeperiodconcept.py
new file mode 100644
index 0000000..893f571
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/timeperiodconcept.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class TimePeriodConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:timePeriod a sdmx:Concept, skos:Concept;
+ # rdfs:label "Time Period"@en;
+ # rdfs:comment """The period of time or point in time to which the measured observation refers."""@en;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:
+ # CROSS_DOMAIN_CONCEPTS[1.0].TIME_PERIOD";
+ # skos:inScheme sdmx-concept:cog.
+ super().__init__(
+ entity_id="timePeriod",
+ label="Time Period",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].TIME_PERIOD",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/titleConcept.py b/sdmx2jsonld/sdmxconcepts/titleConcept.py
new file mode 100644
index 0000000..43fdedf
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/titleConcept.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class TitleConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:title a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Title"@en ;
+ # rdfs:comment """Textual label used as identification of a statistical object."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].TITLE";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="title",
+ label="Title",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].TITLE",
+ )
diff --git a/sdmx2jsonld/sdmxconcepts/unitmultconcept.py b/sdmx2jsonld/sdmxconcepts/unitmultconcept.py
new file mode 100644
index 0000000..e53cd3d
--- /dev/null
+++ b/sdmx2jsonld/sdmxconcepts/unitmultconcept.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxconcepts.sdmxconcept import SDMXConcept
+
+
+class UnitMultConcept(SDMXConcept):
+ def __init__(self):
+ # sdmx-concept:unitMult a sdmx:Concept, skos:Concept ;
+ # rdfs:label "Unit Multiplier"@en ;
+ # rdfs:comment """Exponent in base 10 specified so that multiplying the observation numeric values
+ # by 10^UNIT_MULT gives a value expressed in the UNIT."""@en ;
+ # rdfs:isDefinedBy ;
+ # skos:notation
+ # "urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:CROSS_DOMAIN_CONCEPTS[1.0].UNIT_MULT";
+ # skos:inScheme sdmx-concept:cog .
+ super().__init__(
+ entity_id="unitMult",
+ label="Unit Multiplier",
+ notation="urn:sdmx:org.sdmx.infomodel.conceptscheme.Concept=SDMX:" "CROSS_DOMAIN_CONCEPTS[1.0].UNIT_MULT",
+ )
diff --git a/sdmx2jsonld/sdmxdimensions/__init__.py b/sdmx2jsonld/sdmxdimensions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sdmx2jsonld/sdmxdimensions/frequency.py b/sdmx2jsonld/sdmxdimensions/frequency.py
new file mode 100644
index 0000000..4e44f6c
--- /dev/null
+++ b/sdmx2jsonld/sdmxdimensions/frequency.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassFreqError
+from sdmx2jsonld.sdmxdimensions.sdmxdimension import SDMXDimension
+
+
+class Frequency(SDMXDimension):
+ def __init__(self):
+ # sdmx-dimension:freq a qb:DimensionProperty, rdf:Property;
+ # rdfs:range rdfs:Resource;
+ # qb:concept sdmx-concept:freq;
+ # rdfs:label "Frequency"@en;
+ # rdfs:comment """The time interval at which observations occur over a given time period."""@en;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="freq",
+ label="Frequency",
+ description="The time interval at which observations occur over a given time period.",
+ concept_id="freq",
+ identifier="freq",
+ entity_range="xsd:string",
+ )
+
+ @staticmethod
+ def fix_value(value):
+ # Need to check if the value received is in the list of possible values -> return that value
+ # then maybe could be in the form freq-, so we have to extract the substring and
+ # return that substring if it is in the list of values, if not return an error.
+ # any other value will return an error
+ value_upper = value.upper()
+
+ m = search("FREQ-(.*)", value_upper)
+
+ if m is not None:
+ status = m.group(1)
+ return status
+ else:
+ # We received a value that it is not following the template format
+ raise ClassFreqError(value)
diff --git a/sdmx2jsonld/sdmxdimensions/refarea.py b/sdmx2jsonld/sdmxdimensions/refarea.py
new file mode 100644
index 0000000..d802c18
--- /dev/null
+++ b/sdmx2jsonld/sdmxdimensions/refarea.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.sdmxdimensions.sdmxdimension import SDMXDimension
+
+
+class RefArea(SDMXDimension):
+ def __init__(self):
+ # sdmx-dimension:refArea a qb:DimensionProperty, rdf:Property;
+ # rdfs:range rdfs:Resource;
+ # qb:concept sdmx-concept:refArea;
+ # rdfs:label "Reference Area"@en;
+ # rdfs:comment "The country or geographic area to which the measured statistical phenomenon relates."@en;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="refArea",
+ label="Reference Area",
+ description="The country or geographic area to which the measured statistical " "phenomenon relates.",
+ concept_id="refArea",
+ identifier="refArea",
+ entity_range="xsd:string",
+ )
diff --git a/sdmx2jsonld/sdmxdimensions/sdmxdimension.py b/sdmx2jsonld/sdmxdimensions/sdmxdimension.py
new file mode 100644
index 0000000..dd615c0
--- /dev/null
+++ b/sdmx2jsonld/sdmxdimensions/sdmxdimension.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from sdmx2jsonld.common.commonclass import CommonClass
+
+
+class SDMXDimension(CommonClass):
+ def __init__(
+ self,
+ entity_id,
+ identifier,
+ entity_range,
+ label=None,
+ description=None,
+ concept_id=None,
+ ):
+ super().__init__(entity="DimensionProperty")
+ self.data = {
+ "id": f"urn:ngsi-ld:DimensionProperty:{entity_id}",
+ "type": "DimensionProperty",
+ "language": {"type": "Property", "value": ["en"]},
+ "label": {
+ "type": "Property",
+ "value": {
+ "en": label,
+ },
+ },
+ "description": {
+ "type": "Property",
+ "value": {
+ "en": description,
+ },
+ },
+ "concept": {
+ "type": "Relationship",
+ "object": f"urn:ngsi-ld:Concept:{concept_id}",
+ },
+ "identifier": {"type": "Property", "value": identifier},
+ "range": {"type": "Property", "value": entity_range},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
+ }
+
+ # We need to check if some of the parameters are None, in that case we have to pop the key from data
+ if label is None:
+ self.data.pop("label")
+
+ if description is None:
+ self.data.pop("description")
+
+ if concept_id is None:
+ self.data.pop("concept")
diff --git a/sdmx2jsonld/sdmxdimensions/timeperiod.py b/sdmx2jsonld/sdmxdimensions/timeperiod.py
new file mode 100644
index 0000000..1f112d5
--- /dev/null
+++ b/sdmx2jsonld/sdmxdimensions/timeperiod.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from re import search
+from sdmx2jsonld.exceptions.exceptions import ClassFreqError
+from sdmx2jsonld.sdmxdimensions.sdmxdimension import SDMXDimension
+
+
+class TimePeriod(SDMXDimension):
+ def __init__(self):
+ # sdmx-dimension:timePeriod a qb:DimensionProperty, rdf:Property;
+ # rdfs:range rdfs:Resource;
+ # qb:concept sdmx-concept:timePeriod;
+ # rdfs:label "Time Period"@en;
+ # rdfs:comment "The period of time or point in time to which the measured observation refers."@en;
+ # rdfs:isDefinedBy .
+ super().__init__(
+ entity_id="timePeriod",
+ label="Time Period",
+ description="The period of time or point in time to which the measured observation refers.",
+ concept_id="timePeriod",
+ identifier="timePeriod",
+ entity_range="xsd:string",
+ )
+
+ @staticmethod
+ def fix_value(value):
+ # Need to check if the value received is in the list of possible values -> return that value
+ # then maybe could be in the form freq-, so we have to extract the substring and
+ # return that substring if it is in the list of values, if not return an error.
+ # any other value will return an error
+ value_upper = value.upper()
+
+ m = search("FREQ-(.*)", value_upper)
+
+ if m is not None:
+ status = m.group(1)
+ return status
+ else:
+ # We received a value that it is not following the template format
+ raise ClassFreqError(value)
diff --git a/sdmx2jsonld/transform/attribute.py b/sdmx2jsonld/transform/attribute.py
index f5fc523..f2b3d06 100644
--- a/sdmx2jsonld/transform/attribute.py
+++ b/sdmx2jsonld/transform/attribute.py
@@ -25,11 +25,11 @@
class Attribute(Property):
def __init__(self):
- super().__init__(entity='AttributeProperty')
- self.data['type'] = 'AttributeProperty'
+ super().__init__(entity="AttributeProperty")
+ self.data["type"] = "AttributeProperty"
def add_data(self, attribute_id, data):
- super().add_data(id=id, data=data)
+ super().add_data(property_id=attribute_id, data=data)
# Add the id
- self.data['id'] = "urn:ngsi-ld:AttributeProperty:" + attribute_id
+ self.data["id"] = "urn:ngsi-ld:AttributeProperty:" + attribute_id
diff --git a/sdmx2jsonld/transform/catalogue.py b/sdmx2jsonld/transform/catalogue.py
index 7502ee0..92ef1a7 100644
--- a/sdmx2jsonld/transform/catalogue.py
+++ b/sdmx2jsonld/transform/catalogue.py
@@ -22,29 +22,22 @@
from logging import getLogger
from sdmx2jsonld.common.commonclass import CommonClass
-import random
+from sdmx2jsonld.common.listmanagement import get_rest_data, get_property_value
+from sdmx2jsonld.transform.context import Context
+from random import getrandbits
logger = getLogger()
-class CatalogueDCATAP(CommonClass):
+class Catalogue(CommonClass):
def __init__(self):
- super().__init__(entity='CatalogueDCAT-AP')
+ super().__init__(entity="Catalogue")
self.data = {
"id": str(),
- "type": "CatalogueDCAT-AP",
- "dataset": {
- "type": "object",
- "value": str()
- },
-
- "language": {
- "type": "Property",
- "value": list()
- },
-
-
+ "type": "Catalogue",
+ "dataset": {"type": "Relationship", "object": str()},
+ "language": {"type": "Property", "value": list()},
#################################################
# TODO: New ETSI CIM NGSI-LD specification 1.4.2
# Pending to implement in the Context Broker
@@ -54,45 +47,126 @@ def __init__(self):
# "LanguageMap": dict()
# },
#################################################
- "description": {
- "type": "Property",
- "value": dict()
- },
-
- "publisher": {
- "type": "Property",
- "value": str()
- },
-
- "title": {
- "type": "Array",
- "value": list()
- },
-
-
+ "description": {"type": "Property", "value": dict()},
+ "publisher": {"type": "Property", "value": str()},
+ "title": {"type": "Property", "value": list()},
"@context": [
- "https://raw.githubusercontent.com/SEMICeu/DCAT-AP/master/releases/1.1/dcat-ap_1.1.jsonld",
- "https://raw.githubusercontent.com/smart-data-models/dataModel.DCAT-AP/master/context.jsonld"
- ]
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
}
self.concept_id = str()
+ self.keys = {k: k for k in self.data.keys()}
def add_dataset(self, dataset_id):
self.concept_id = dataset_id
# generate hash id
- random_bits = random.getrandbits(128)
+ random_bits = getrandbits(128)
hash1 = "%032x" % random_bits
# Add the id
- self.data['id'] = "urn:ngsi-ld:CatalogueDCAT-AP:" + hash1
+ self.data["id"] = "urn:ngsi-ld:Catalogue:" + hash1
# Add dataset id
- self.data['dataset']['value'] = dataset_id
+ self.data["dataset"]["object"] = dataset_id
+
+ def add_data(self, title, dataset_id, data):
+ # We need to complete the data corresponding to the Catalogue: rdfs:label
+ self.__complete_label__(title=title, data=data)
+
+ # Add the title
+ key = self.keys["title"]
+ self.data[key]["value"] = title
+
+ # Add the id
+ self.data["id"] = "urn:ngsi-ld:Catalogue:" + dataset_id
+
+ # Add the publisher
+ key = self.get_key(requested_key="dcterms:publisher")
+ position = data.index(key) + 1
+ self.data["publisher"]["value"] = data[position][0]
+
+ # Check if we have 'issued' in the original, then we need to create the releaseDate property
+ index, key, value = get_property_value(data=data, property_name="issued")
+ if index != -1:
+ # We found an 'issued' data
+ self.data.update(self.__generate_property__(key="releaseDate", value=value[0][0]))
+
+ # Get the rest of the data, qb:structure has the same value of qb:dataset, so we decide to
+ # use only qb:dataset in CatalogueDCAT-AP
+ data = get_rest_data(
+ data=data,
+ not_allowed_keys=["label", "publisher", "structure", "issued", "title"],
+ )
+
+ # add the new data to the dataset structure
+ self.patch_data(data, False)
+
+ # Order Context keys
+ a = Context()
+ a.set_data(new_data=self.data)
+ a.order_context()
+ self.data = a.get_data()
+
+ def patch_data(self, data, language_map):
+ if language_map:
+ self.__complete_label__(title="Not specified", data=data)
+ else:
+ # TODO: Add only those properties that are expected, if they are not know or unexpected discard and provide
+ # a logging about the property is discarded due to it is not considered in the statSCAT-AP spec.
+ [self.data.update(self.__generate_property__(key=k, value=v)) for k, v in data.items()]
+
+ def __complete_label__(self, title, data):
+ try:
+ key = self.get_key(requested_key="rdfs:label")
+ position = data.index(key) + 1
+ description = data[position]
+
+ descriptions = [x[0].replace('"', "") for x in description]
+
+ languages = list()
+ try:
+ languages = [x[1].replace("@", "").lower() for x in description]
+ except IndexError:
+ logger.warning(f"The Catalogue {title} has a " f"rdfs:label without language tag: {description}")
+
+ aux = len(description)
+ if aux != 1:
+ logger.error(f"Catalogue: there is more than 1 description ({aux}), values: {description}")
+ else:
+ # There is no language tag, we use by default 'en'
+ languages = ["en"]
+ logger.warning('Catalogue: selecting default language "en"')
+
+ ###############################################################################
+ # TODO: New ETSI CIM NGSI-LD specification 1.4.2
+ # Pending to implement in the Context Broker
+ ###############################################################################
+ # for i in range(0, len(languages)):
+ # self.data['rdfs:label']['LanguageMap'][languages[i]] = descriptions[i]
+ ###############################################################################
+ for i in range(0, len(languages)):
+ key = self.keys["description"]
+ self.data[key]["value"][languages[i]] = descriptions[i]
+
+ # Complete the information of the language with the previous information
+ key = self.keys["language"]
+ self.data[key]["value"] = languages
+ except ValueError:
+ logger.info(f"Dataset without rdfs:label detail: {title}")
def get(self):
return self.data
def get_id(self):
- return self.data['id']
+ return self.data["id"]
+
+ def get_key(self, requested_key):
+ try:
+ key = self.keys[requested_key]
+ return key
+ except KeyError:
+ # The key did not exist therefore we add to the list with this value
+ self.keys[requested_key] = requested_key
+ return requested_key
diff --git a/sdmx2jsonld/transform/concept.py b/sdmx2jsonld/transform/concept.py
index db3b126..3e7c54c 100644
--- a/sdmx2jsonld/transform/concept.py
+++ b/sdmx2jsonld/transform/concept.py
@@ -30,24 +30,14 @@
class Concept(CommonClass):
def __init__(self):
- super().__init__(entity='Concept')
+ super().__init__(entity="Concept")
self.data = {
"id": str(),
"type": "Concept",
- "dct:language": {
- "type": "Property",
- "value": list()
- },
- "skos:inScheme": {
- "type": "Relationship",
- "value": str()
- },
- "rdfs:subClassOf": {
- "type": "Property",
- "value": str()
- },
-
+ "language": {"type": "Property", "value": list()},
+ "inScheme": {"type": "Relationship", "object": str()},
+ "rdfs:subClassOf": {"type": "Property", "value": str()},
#################################################
# TODO: New ETSI CIM NGSI-LD specification 1.4.2
# Pending to implement in the Context Broker
@@ -57,16 +47,14 @@ def __init__(self):
# "LanguageMap": dict()
# },
#################################################
- "skos:prefLabel": {
- "type": "Property",
- "value": dict()
- },
-
-
- "@context": dict()
+ "prefLabel": {"type": "Property", "value": dict()},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
}
self.concept_id = str()
+ self.keys = {k: k for k in self.data.keys()}
def add_data(self, concept_id, data):
# TODO: We have to control that data include the indexes that we want to search
@@ -76,29 +64,30 @@ def add_data(self, concept_id, data):
self.concept_id = concept_id
try:
- position = data.index('skos:prefLabel') + 1
+ position = data.index("skos:prefLabel") + 1
except ValueError:
# We could not find skos:prefLabel, try to find rdfs:label
- position = data.index('rdfs:label') + 1
- logger.warning(f'The Concept {concept_id} does not contain skos:prefLabel but rdfs:label. We use its '
- f'content to fill in the skos:prefLabel property')
+ position = data.index("rdfs:label") + 1
+ logger.warning(
+ f"The Concept {concept_id} does not contain skos:prefLabel but rdfs:label. We use its "
+ f"content to fill in the skos:prefLabel property"
+ )
description = data[position]
- descriptions = [x[0].replace("\"", "") for x in description]
+ descriptions = [x[0].replace('"', "") for x in description]
languages = list()
try:
languages = [x[1].replace("@", "").lower() for x in description]
except IndexError:
- logger.warning(f'The Concept {concept_id} has a '
- f'skos:prefLabel without language tag: {description}')
+ logger.warning(f"The Concept {concept_id} has a " f"skos:prefLabel without language tag: {description}")
aux = len(description)
if aux != 1:
logger.error(f"Concept: there is more than 1 description ({aux}), values: {description}")
else:
# There is no language tag, we use by default 'en'
- languages = ['en']
+ languages = ["en"]
logger.warning('Concept: selecting default language "en"')
# Complete the skos:prefLabel
@@ -110,14 +99,14 @@ def add_data(self, concept_id, data):
# self.data['skos:prefLabel']['LanguageMap'][languages[i]] = descriptions[i]
###############################################################################
for i in range(0, len(languages)):
- self.data['skos:prefLabel']['value'][languages[i]] = descriptions[i]
+ self.data["prefLabel"]["value"][languages[i]] = descriptions[i]
# Complete the information of the language with the previous information
- key = self.keys['dct:language']
- self.data[key]['value'] = languages
+ key = self.keys["language"]
+ self.data[key]["value"] = languages
# Add the id
- self.data['id'] = "urn:ngsi-ld:Concept:" + concept_id
+ self.data["id"] = "urn:ngsi-ld:Concept:" + concept_id
# rdfs:seeAlso
self.need_add_in_scheme(data=data)
@@ -128,10 +117,9 @@ def add_data(self, concept_id, data):
# skos:notation
self.need_add_notation(data=data)
- # Simplify Context and order keys
+ # Order the keys in the final json-ld
a = Context()
- a.set_data(data=self.data)
- a.new_analysis()
+ a.set_data(new_data=self.data)
a.order_context()
self.data = a.get_data()
@@ -139,46 +127,51 @@ def get(self):
return self.data
def get_id(self):
- return self.data['id']
+ return self.data["id"]
def need_add_subclass(self, data):
try:
- position = data.index('rdfs:subClassOf') + 1
- self.data['rdfs:subClassOf']['value'] = data[position][0]
+ position = data.index("rdfs:subClassOf") + 1
+ self.data["rdfs:subClassOf"]["value"] = data[position][0]
except ValueError:
- logger.info(f'The Concept {self.concept_id} has no rdfs:subClassOf property, deleting the key in the data')
+ logger.info(f"The Concept {self.concept_id} has no rdfs:subClassOf property, deleting the key in the data")
# We delete the "rdfs:subClassOf" property from the final structure
- self.data.pop('rdfs:subClassOf')
+ self.data.pop("rdfs:subClassOf")
def need_add_in_scheme(self, data):
position = 0
try:
- position = data.index('rdfs:seeAlso') + 1
+ position = data.index("rdfs:seeAlso") + 1
except ValueError:
# We will try to find the skos:inScheme
try:
- position = data.index('skos:inScheme') + 1
+ position = data.index("skos:inScheme") + 1
except ValueError:
- logger.info(f'The Concept {self.concept_id} has neither rdfs:seeAlso or skos:inScheme properties, '
- f'deleting the key in the data')
+ logger.info(
+ f"The Concept {self.concept_id} has neither rdfs:seeAlso or skos:inScheme properties, "
+ f"deleting the key in the data"
+ )
# We delete the "skos:inScheme" property from the final structure
- self.data.pop('skos:inScheme')
+ self.data.pop("skos:inScheme")
parser = RegParser()
concept_schema = data[position][0]
concept_schema = "urn:ngsi-ld:ConceptSchema:" + parser.obtain_id(concept_schema)
- self.data['skos:inScheme']['value'] = concept_schema
+ if self.data["inScheme"]["type"] == "Relationship":
+ self.data["inScheme"]["object"] = concept_schema
+ else:
+ self.data["inScheme"]["value"] = concept_schema
def need_add_notation(self, data):
try:
- position = data.index('skos:notation') + 1
+ position = data.index("skos:notation") + 1
- self.data['skos:notation'] = {
- 'type': 'Property',
- 'value': data[position][0][0].replace("\"", "")
+ self.data["notation"] = {
+ "type": "Property",
+ "value": data[position][0][0].replace('"', ""),
}
except ValueError:
- logger.info(f'The Concept {self.concept_id} has no skos:notation property')
+ logger.info(f"The Concept {self.concept_id} has no skos:notation property")
diff --git a/sdmx2jsonld/transform/conceptschema.py b/sdmx2jsonld/transform/conceptschema.py
index 8388259..6764411 100644
--- a/sdmx2jsonld/transform/conceptschema.py
+++ b/sdmx2jsonld/transform/conceptschema.py
@@ -23,27 +23,20 @@
from logging import getLogger
from sdmx2jsonld.common.commonclass import CommonClass
from sdmx2jsonld.transform.context import Context
+from sdmx2jsonld.common.listmanagement import flatten_value
logger = getLogger()
class ConceptSchema(CommonClass):
def __init__(self):
- super().__init__(entity='ConceptScheme')
+ super().__init__(entity="ConceptScheme")
self.data = {
"id": str(),
"type": "ConceptScheme",
- "dct:language": {
- "type": "Property",
- "value": list()
- },
- "skos:hasTopConcept": {
- "type": "Property",
- "value": list()
- },
-
-
+ "language": {"type": "Property", "value": list()},
+ "hasTopConcept": {"type": "Relationship", "object": list()},
#################################################
# TODO: New ETSI CIM NGSI-LD specification 1.4.2
# Pending to implement in the Context Broker
@@ -53,36 +46,36 @@ def __init__(self):
# "LanguageMap": dict()
# },
#################################################
- "skos:prefLabel": {
- "type": "Property",
- "value": dict()
- },
-
-
- "@context": dict()
+ "prefLabel": {"type": "Property", "value": dict()},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
}
+ self.keys = {k: k for k in self.data.keys()}
+
def add_data(self, concept_schema_id, data):
# TODO: We have to control that data include the indexes that we want to search
# We need to complete the data corresponding to the ConceptSchema: skos:prefLabel
- position = data.index('skos:prefLabel') + 1
+ position = data.index("skos:prefLabel") + 1
description = data[position]
- descriptions = [x[0].replace("\"", "") for x in description]
+ descriptions = [x[0].replace('"', "") for x in description]
languages = list()
try:
languages = [x[1].replace("@", "").lower() for x in description]
except IndexError:
- logger.warning(f'The ConceptSchema {concept_schema_id} has a '
- f'skos:prefLabel without language tag: {description}')
+ logger.warning(
+ f"The ConceptSchema {concept_schema_id} has a " f"skos:prefLabel without language tag: {description}"
+ )
aux = len(description)
if aux != 1:
logger.error(f"ConceptSchema: there is more than 1 description ({aux}), values: {description}")
else:
# There is no language tag, we use by default 'en'
- languages = ['en']
+ languages = ["en"]
logger.warning('ConceptSchema: selecting default language "en"')
# Complete the skos:prefLabel
@@ -94,25 +87,43 @@ def add_data(self, concept_schema_id, data):
# self.data['skos:prefLabel']['LanguageMap'][languages[i]] = descriptions[i]
###############################################################################
for i in range(0, len(languages)):
- self.data['skos:prefLabel']['value'][languages[i]] = descriptions[i]
+ self.data["prefLabel"]["value"][languages[i]] = descriptions[i]
# Complete the information of the language with the previous information
- key = self.keys['dct:language']
- self.data[key]['value'] = languages
+ key = self.keys["language"]
+ self.data[key]["value"] = languages
# Add the id
- self.data['id'] = "urn:ngsi-ld:ConceptSchema:" + concept_schema_id
+ self.data["id"] = "urn:ngsi-ld:ConceptSchema:" + concept_schema_id
# TODO: We need to control that the concept id extracted here are the same that we analyse afterwards.
# skos:hasTopConcept, this is a list of ids
- position = data.index('skos:hasTopConcept') + 1
- result = list(map(lambda x: self.generate_id(value=x, entity='Concept'), data[position]))
- self.data['skos:hasTopConcept']['value'] = result
+ position = data.index("skos:hasTopConcept") + 1
+ result = list(map(lambda x: self.generate_id(value=x, entity="Concept"), data[position]))
+ self.data["hasTopConcept"]["object"] = result
+
+ # Get the rest of data, dct:created and dct:modified properties
+ try:
+ position = data.index("dct:created") + 1
+ self.data["created"] = {
+ "type": "Property",
+ "value": flatten_value(data[position]),
+ }
+ except ValueError:
+ logger.warning(f"dct:created is not present in the Concept Schema: {concept_schema_id}")
+
+ try:
+ position = data.index("dct:modified") + 1
+ self.data["modified"] = {
+ "type": "Property",
+ "value": flatten_value(data[position]),
+ }
+ except ValueError:
+ logger.warning(f"dct:modified is not present in the Concept Schema: {concept_schema_id}")
- # Simplify Context and order keys
+ # Order the keys in the final json-ld
a = Context()
- a.set_data(data=self.data)
- a.new_analysis()
+ a.set_data(new_data=self.data)
a.order_context()
self.data = a.get_data()
diff --git a/sdmx2jsonld/transform/context.py b/sdmx2jsonld/transform/context.py
index 6465af1..60d7ed7 100644
--- a/sdmx2jsonld/transform/context.py
+++ b/sdmx2jsonld/transform/context.py
@@ -20,17 +20,16 @@
# under the License.
##
+
class Context:
def __init__(self):
- self.context = {
- "@context": dict()
- }
+ self.context = {"@context": dict()}
self.fixed_context = [
- 'https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld',
- 'http://www.w3.org/ns/dcat#',
- 'http://data.europa.eu/(xyz)/statdcat-ap/',
- 'http://purl.org/dc/terms/'
+ "https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld",
+ "http://www.w3.org/ns/dcat#",
+ "http://data.europa.eu/(xyz)/statdcat-ap/",
+ "http://purl.org/dc/terms/",
]
# Dictionary to keep those contexts that are update from the core contexts
@@ -38,18 +37,16 @@ def __init__(self):
self.data = dict()
# By default, the context should include the smart data models context
- self.context['@context']\
- .update({'sdmp': 'https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld'})
+ self.context["@context"].update(
+ {"sdmp": "https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld"}
+ )
# statDCAT-AP contexts
- self.context['@context']\
- .update({'dcat': 'http://www.w3.org/ns/dcat#'})
+ self.context["@context"].update({"dcat": "http://www.w3.org/ns/dcat#"})
- self.context['@context']\
- .update({'dct': 'http://purl.org/dc/terms/'})
+ self.context["@context"].update({"dct": "http://purl.org/dc/terms/"})
- self.context['@context']\
- .update({'stat': 'http://data.europa.eu/(xyz)/statdcat-ap/'})
+ self.context["@context"].update({"stat": "http://data.europa.eu/(xyz)/statdcat-ap/"})
def add_context(self, context):
aux = list(context.items())
@@ -57,20 +54,21 @@ def add_context(self, context):
value = aux[0][1]
found = False
+ k = ""
# check if the value of the new_context is in one of the values of the previous context
- for k, v in self.context['@context'].items():
+ for k, v in self.context["@context"].items():
if v == value:
found = True
break
if not found:
# we did not find a key -> New context, we need to add
- self.context['@context'].update(context)
+ self.context["@context"].update(context)
else:
# We found then we need to change the key in the context or add new one and delete the old one
- self.context['@context'].pop(k)
- self.context['@context'].update(context)
+ self.context["@context"].pop(k)
+ self.context["@context"].update(context)
self.context_mapping.update({k: key})
def get_context(self):
@@ -84,35 +82,35 @@ def print_context(self):
def key_used(self):
def key(json_property):
- aux = json_property.split(':')
+ aux = json_property.split(":")
if len(aux) == 2:
aux = aux[0]
else:
- aux = ''
+ aux = ""
return aux
# Get the list of keys except id, type, and @context
keys = list(self.data.keys())
- keys.remove('id')
- keys.remove('type')
- keys.remove('@context')
+ keys.remove("id")
+ keys.remove("type")
+ keys.remove("@context")
prefix_ids = list(map(lambda x: key(json_property=x), keys))
- prefix_ids = list(filter(lambda x: x != '', prefix_ids))
+ prefix_ids = list(filter(lambda x: x != "", prefix_ids))
prefix_ids = [*set(prefix_ids)]
return prefix_ids
def reduce_context(self, used_keys):
# 1st: Get the key-value of the fixed context
- aux = dict((new_val, new_k) for new_k, new_val in self.data['@context'].items())
+ aux = dict((new_val, new_k) for new_k, new_val in self.data["@context"].items())
aux = list(map(lambda x: aux[x], self.fixed_context))
# 2nd: Join fixed_context and used_keys
aux = aux + used_keys
# 3rd: Get the new context
- new_context = list(map(lambda x: {x: self.data['@context'][x]}, aux))
+ new_context = list(map(lambda x: {x: self.data["@context"][x]}, aux))
new_context = dict((key, val) for k in new_context for key, val in k.items())
# TODO: we should fix if the rest of context lines are needed or they are duplicated some properties
@@ -130,18 +128,18 @@ def new_analysis(self):
used_keys = self.key_used()
new_context = self.reduce_context(used_keys=used_keys)
- self.data['@context'] = new_context
+ self.data["@context"] = new_context
def order_context(self):
# I want that the content of the dict, 1st id, 2nd type, last context
# Get all the keys and initialize the order
keys = list(self.data.keys())
- keys.remove('type')
- keys.remove('id')
- keys.remove('@context')
+ keys.remove("type")
+ keys.remove("id")
+ keys.remove("@context")
# initializing order
- ord_list = ['id', 'type'] + keys + ['@context']
+ ord_list = ["id", "type"] + keys + ["@context"]
# Custom order dictionary
# Using dictionary comprehension
@@ -150,33 +148,49 @@ def order_context(self):
def get_data(self):
return self.data
- def set_data(self, data):
- self.data = data
+ def set_data(self, new_data):
+ self.data = new_data
-if __name__ == '__main__':
+if __name__ == "__main__":
a = Context()
a.print_context()
- a.add_context({'rdf': ''})
+ a.add_context({"rdf": ""})
a.print_context()
print()
- data = {'type': 'Dataset', 'id': 'urn:ngsi-ld:Dataset:dsd3001',
- 'dc:title': 'http://bauhaus/structuresDeDonnees/structure/dsd3001', 'dc:identifier': 'dsd3001',
- 'dc:language': {'type': 'Property', 'value': ['en', 'fr']},
- 'dc:description': {'type': 'Property', 'value': {'en': 'SDMX DSD NA_MAIN', 'fr': 'SDMX NA_MAIN'}},
- '@context': {'sdmp': 'https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld',
- 'dcat': 'http://www.w3.org/ns/dcat#', 'stat': 'http://data.europa.eu/(xyz)/statdcat-ap/',
- 'qb': 'http://purl.org/linked-data/cube#', 'dc11': 'http://purl.org/dc/elements/1.1/',
- 'dc': 'http://purl.org/dc/terms/', 'xsd': 'http://www.w3.org/2001/XMLSchema#',
- 'ns0': 'http://rdf.insee.fr/def/base#', 'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
- 'skos': 'http://www.w3.org/2004/02/skos/core#', 'owl': 'http://www.w3.org/2002/07/owl#'},
- 'dc11:contributor': 'DG75-H250', 'dc11:creator': 'DG57-L201', 'dc:created': '2022-01-15T08:00:00+00:00',
- 'dc:modified': '2022-01-15T10:00:00+00:00',
- 'other_thing': 'foo'}
-
- a.set_data(data=data)
+ data = {
+ "type": "Dataset",
+ "id": "urn:ngsi-ld:Dataset:dsd3001",
+ "dc:title": "http://bauhaus/structuresDeDonnees/structure/dsd3001",
+ "dc:identifier": "dsd3001",
+ "dc:language": {"type": "Property", "value": ["en", "fr"]},
+ "dc:description": {
+ "type": "Property",
+ "value": {"en": "SDMX DSD NA_MAIN", "fr": "SDMX NA_MAIN"},
+ },
+ "@context": {
+ "sdmp": "https://smart-data-models.github.io/dataModel.STAT-DCAT-AP/context.jsonld",
+ "dcat": "http://www.w3.org/ns/dcat#",
+ "stat": "http://data.europa.eu/(xyz)/statdcat-ap/",
+ "qb": "http://purl.org/linked-data/cube#",
+ "dc11": "http://purl.org/dc/elements/1.1/",
+ "dc": "http://purl.org/dc/terms/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "ns0": "http://rdf.insee.fr/def/base#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "skos": "http://www.w3.org/2004/02/skos/core#",
+ "owl": "http://www.w3.org/2002/07/owl#",
+ },
+ "dc11:contributor": "DG75-H250",
+ "dc11:creator": "DG57-L201",
+ "dc:created": "2022-01-15T08:00:00+00:00",
+ "dc:modified": "2022-01-15T10:00:00+00:00",
+ "other_thing": "foo",
+ }
+
+ a.set_data(new_data=data)
a.new_analysis()
print(a.get_data())
a.order_context()
diff --git a/sdmx2jsonld/transform/dataset.py b/sdmx2jsonld/transform/dataset.py
index 15cc49b..3d87a6f 100644
--- a/sdmx2jsonld/transform/dataset.py
+++ b/sdmx2jsonld/transform/dataset.py
@@ -25,24 +25,73 @@
from sdmx2jsonld.common.listmanagement import get_rest_data
from sdmx2jsonld.transform.context import Context
+from sdmx2jsonld.sdmxdimensions.frequency import Frequency
+from sdmx2jsonld.sdmxdimensions.refarea import RefArea
+from sdmx2jsonld.sdmxdimensions.timeperiod import TimePeriod
+
+from sdmx2jsonld.sdmxattributes.observationStatus import ObsStatus
+from sdmx2jsonld.sdmxattributes.confirmationStatus import ConfStatus
+from sdmx2jsonld.sdmxattributes.timeFormat import TimeFormat
+from sdmx2jsonld.sdmxattributes.timePerCollect import TimePerCollect
+from sdmx2jsonld.sdmxattributes.decimals import Decimals
+from sdmx2jsonld.sdmxattributes.title import Title
+from sdmx2jsonld.sdmxattributes.unitmult import UnitMult
+from sdmx2jsonld.sdmxattributes.compilingorg import CompilingOrg
+from sdmx2jsonld.sdmxattributes.dataComp import DataComp
+from sdmx2jsonld.sdmxattributes.currency import Currency
+from sdmx2jsonld.sdmxattributes.dissorg import DissOrg
+
+from sdmx2jsonld.sdmxconcepts.freqconcept import FreqConcept
+from sdmx2jsonld.sdmxconcepts.cogconceptschema import CogConceptSchema
+from sdmx2jsonld.sdmxconcepts.timeperiodconcept import TimePeriodConcept
+from sdmx2jsonld.sdmxconcepts.refareaconcept import RefAreaConcept
+from sdmx2jsonld.sdmxconcepts.obsstatusconcept import ObsStatusConcept
+from sdmx2jsonld.sdmxconcepts.confstatusconcept import ConfStatusConcept
+from sdmx2jsonld.sdmxconcepts.timeformatconcept import TimeFormatConcept
+from sdmx2jsonld.sdmxconcepts.timePerCollectConcept import TimePerCollectConcept
+from sdmx2jsonld.sdmxconcepts.decimals import DecimalsConcept
+from sdmx2jsonld.sdmxconcepts.titleConcept import TitleConcept
+from sdmx2jsonld.sdmxconcepts.unitmultconcept import UnitMultConcept
+from sdmx2jsonld.sdmxconcepts.compilingorgconcept import CompilingOrgConcept
+from sdmx2jsonld.sdmxconcepts.datacompconcept import DataCompConcept
+from sdmx2jsonld.sdmxconcepts.currencyconcept import CurrencyConcept
+from sdmx2jsonld.sdmxconcepts.dissorgconcept import DissOrgConcept
+
+from sdmx2jsonld.cube.measuretype import MeasureType
+
logger = getLogger()
class Dataset(CommonClass):
def __init__(self):
- super().__init__(entity='Dataset')
+ super().__init__(entity="Dataset")
+
+ # TODO: These dimensions are not defined in the turtle file but defined in a prefix therefore at the moment
+ # we create manually their corresponding DimensionProperty entity. Should we generated from checking the prefix
+ self.list_special_components = [
+ "freq",
+ "refArea",
+ "timePeriod",
+ "obsStatus",
+ "confStatus",
+ "timeFormat",
+ "timePerCollect",
+ "decimals",
+ "title",
+ "unitMult",
+ "compilingOrg",
+ "dataComp",
+ "currency",
+ "dissOrg",
+ "measureType",
+ ]
self.data = {
"id": str(),
"type": "Dataset",
- "dct:title": str(),
- "dct:identifier": str(),
- "dct:language": {
- "type": "Property",
- "value": list()
- },
-
-
+ "title": {"type": "Property", "value": str()},
+ "identifier": str(),
+ "language": {"type": "Property", "value": list()},
#################################################
# TODO: New ETSI CIM NGSI-LD specification 1.4.2
# Pending to implement in the Context Broker
@@ -52,86 +101,137 @@ def __init__(self):
# "LanguageMap": dict()
# },
#################################################
- "dct:description": {
- "type": "Property",
- "value": dict()
- },
-
-
- "@context": dict()
+ "description": {"type": "Property", "value": dict()},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
}
self.components = {
- 'qb:attribute': {
- 'entity': 'AttributeProperty',
- 'key': 'stat:attribute',
- 'value': {
- "stat:attribute": {
- "type": "Property",
- "value": list()
- }
- }
+ "qb:attribute": {
+ "entity": "AttributeProperty",
+ "key": "attribute",
+ "value": {"attribute": {"type": "Relationship", "object": list()}},
},
- 'qb:dimension': {
- 'entity': 'DimensionProperty',
- 'key': 'stat:dimension',
- 'value': {
- "stat:dimension": {
- "type": "Property",
- "value": list()
- }
- }
+ "qb:dimension": {
+ "entity": "DimensionProperty",
+ "key": "dimension",
+ "value": {"dimension": {"type": "Relationship", "object": list()}},
},
- 'qb:measure': {
- 'entity': 'Measure',
- 'key': 'stat:statUnitMeasure',
- 'value': {
- "stat:statUnitMeasure": {
- "type": "Property",
- "value": list()
- }
- }
- }
+ "qb:measure": {
+ "entity": "statUnitMeasure",
+ "key": "statUnitMeasure",
+ "value": {"statUnitMeasure": {"type": "Relationship", "object": list()}},
+ },
+ }
+
+ self.keys = (
+ {k: k for k in self.data.keys()}
+ | {self.components["qb:attribute"]["key"]: self.components["qb:attribute"]["key"]}
+ | {self.components["qb:dimension"]["key"]: self.components["qb:dimension"]["key"]}
+ | {self.components["qb:measure"]["key"]: self.components["qb:measure"]["key"]}
+ )
+
+ self.sdmx_dimensions = {
+ "freq": Frequency(),
+ "refArea": RefArea(),
+ "timePeriod": TimePeriod(),
+ "measureType": MeasureType(),
+ }
+
+ self.sdmx_attributes = {
+ "obsStatus": ObsStatus(),
+ "confStatus": ConfStatus(),
+ "timeFormat": TimeFormat(),
+ "timePerCollect": TimePerCollect(),
+ "decimals": Decimals(),
+ "title": Title(),
+ "unitMult": UnitMult(),
+ "compilingOrg": CompilingOrg(),
+ "dataComp": DataComp(),
+ "dissOrg": DissOrg(),
+ "currency": Currency(),
+ }
+
+ self.sdmx_components = {
+ "DimensionProperty": self.sdmx_dimensions,
+ "AttributeProperty": self.sdmx_attributes,
+ }
+
+ self.sdmx_concepts = {
+ "freq": FreqConcept(),
+ "refArea": RefAreaConcept(),
+ "timePeriod": TimePeriodConcept(),
+ "obsStatus": ObsStatusConcept(),
+ "confStatus": ConfStatusConcept(),
+ "timeFormat": TimeFormatConcept(),
+ "timePerCollect": TimePerCollectConcept(),
+ "decimals": DecimalsConcept(),
+ "title": TitleConcept(),
+ "unitMult": UnitMultConcept(),
+ "compilingOrg": CompilingOrgConcept(),
+ "dataComp": DataCompConcept(),
+ "dissOrg": DissOrgConcept(),
+ "currency": CurrencyConcept(),
+ "measureType": None,
}
- self.keys = {k: k for k in self.data.keys()} | \
- {self.components['qb:attribute']['key']: self.components['qb:attribute']['key']} | \
- {self.components['qb:dimension']['key']: self.components['qb:dimension']['key']} | \
- {self.components['qb:measure']['key']: self.components['qb:measure']['key']}
+ self.sdmx_concept_schemas = CogConceptSchema()
- def add_components(self, context, component):
+ def add_components(self, component):
# We need to know which kind of component we have, it should be the verb:
# qb:attribute, qb:dimension, or qb:measure
- list_components = ['qb:attribute', 'qb:dimension', 'qb:measure']
+ list_components = ["qb:attribute", "qb:dimension", "qb:measure"]
+
type_component = [x for x in list_components if x in component][0]
position = component.index(type_component) + 1
+ if type_component == "qb:measure":
+ logger.info(f'The qb:measure "{component[position][0]}" is not manage in statDCAT-AP')
+ new_component, new_concept, new_concept_schema = None, None, None
+ else:
+ new_component, new_concept, new_concept_schema = self.manage_components(
+ type_component=type_component, component=component, position=position
+ )
+
+ return new_component, new_concept, new_concept_schema
+
+ def manage_components(self, type_component, component, position):
+ new_component, new_concept, new_concept_schema = None, None, None
try:
- entity = self.components[type_component]['entity']
- new_id = self.generate_id(entity=entity, value=component[position][0])
- key = self.components[type_component]['key']
+ entity = self.components[type_component]["entity"]
+ name, new_id = self.generate_id(entity=entity, value=component[position][0], update_id=False)
+ key = self.components[type_component]["key"]
# It is possible that the original file contains already the description
- if new_id in self.components[type_component]['value'][key]['value']:
+ if new_id in self.components[type_component]["value"][key]["object"]:
+ logger.warning(f"The component {new_id} is duplicated and already defined in the {self.data['id']}")
+ elif name in self.list_special_components:
+ # We need to create manually the description of these dimensions, concepts, and conceptschemas
logger.warning(
- f"The component {new_id} is duplicated and already defined in the {self.data['id']}")
+ f"The component {name} is defined probably outside of the file, "
+ f"creating manually the {entity} entity"
+ )
+ self.components[type_component]["value"][key]["object"].append(new_id)
+ self.data = self.data | self.components[type_component]["value"]
+
+ new_component = self.sdmx_components[entity][name]
+ new_concept = self.sdmx_concepts[name]
+ new_concept_schema = self.sdmx_concept_schemas
else:
- self.components[type_component]['value'][key]['value'].append(new_id)
- self.data = self.data | self.components[type_component]['value']
+ self.components[type_component]["value"][key]["object"].append(new_id)
+ self.data = self.data | self.components[type_component]["value"]
except ValueError:
logger.error(f"Error, it was identified a qb:ComponentSpecification with a wrong type: {type_component}")
- # Simplify Context amd order keys. It is possible that we call add_component before the dataset has been created
- # therefore we need to add the corresponding context to the dataset
- if len(self.data['@context']) == 0:
- self.data['@context'] = context['@context']
-
+ # Order the keys in the final json-ld
a = Context()
- a.set_data(data=self.data)
- a.new_analysis()
+ a.set_data(new_data=self.data)
a.order_context()
self.data = a.get_data()
+ return new_component, new_concept, new_concept_schema
+
def get(self):
return self.data
@@ -140,26 +240,25 @@ def add_data(self, title, dataset_id, data):
self.__complete_label__(title=title, data=data)
# Add the title
- key = self.keys['dct:title']
- self.data[key] = title
+ key = self.keys["title"]
+ self.data[key]["value"] = title
# Add the id
- self.data['id'] = "urn:ngsi-ld:Dataset:" + dataset_id
+ self.data["id"] = "urn:ngsi-ld:Dataset:" + dataset_id
# Get the rest of the data
- data = get_rest_data(data=data,
- not_allowed_keys=[
- 'sliceKey',
- 'component',
- 'disseminationStatus',
- 'validationState',
- 'notation',
- 'label'
- ],
- further_process_keys=[
- 'component',
- 'label'
- ])
+ data = get_rest_data(
+ data=data,
+ not_allowed_keys=[
+ "sliceKey",
+ "component",
+ "disseminationStatus",
+ "validationState",
+ "notation",
+ "label",
+ ],
+ further_process_keys=["component", "label"],
+ )
# add the new data to the dataset structure
self.patch_data(data, False)
@@ -170,29 +269,28 @@ def patch_data(self, data, language_map):
else:
# TODO: Add only those properties that are expected, if they are not know or unexpected discard and provide
# a logging about the property is discarded due to it is not considered in the statSCAT-AP spec.
- [self.data.update({k: v}) for k, v in data.items()]
+ [self.data.update(self.__generate_property__(key=k, value=v)) for k, v in data.items()]
def __complete_label__(self, title, data):
try:
- key = self.get_key(requested_key='rdfs:label')
+ key = self.get_key(requested_key="rdfs:label")
position = data.index(key) + 1
description = data[position]
- descriptions = [x[0].replace("\"", "") for x in description]
+ descriptions = [x[0].replace('"', "") for x in description]
languages = list()
try:
languages = [x[1].replace("@", "").lower() for x in description]
except IndexError:
- logger.warning(f'The Dataset {title} has a '
- f'rdfs:label without language tag: {description}')
+ logger.warning(f"The Dataset {title} has a " f"rdfs:label without language tag: {description}")
aux = len(description)
if aux != 1:
logger.error(f"Dataset: there is more than 1 description ({aux}), values: {description}")
else:
# There is no language tag, we use by default 'en'
- languages = ['en']
+ languages = ["en"]
logger.warning('Dataset: selecting default language "en"')
###############################################################################
@@ -203,14 +301,14 @@ def __complete_label__(self, title, data):
# self.data['rdfs:label']['LanguageMap'][languages[i]] = descriptions[i]
###############################################################################
for i in range(0, len(languages)):
- key = self.keys['dct:description']
- self.data[key]['value'][languages[i]] = descriptions[i]
+ key = self.keys["description"]
+ self.data[key]["value"][languages[i]] = descriptions[i]
# Complete the information of the language with the previous information
- key = self.keys['dct:language']
- self.data[key]['value'] = languages
+ key = self.keys["language"]
+ self.data[key]["value"] = languages
except ValueError:
- logger.info(f'DataStructureDefinition without rdfs:label detail: {title}')
+ logger.info(f"DataStructureDefinition without rdfs:label detail: {title}")
def get_key(self, requested_key):
try:
diff --git a/sdmx2jsonld/transform/dimension.py b/sdmx2jsonld/transform/dimension.py
index bbbd68c..850f3e0 100644
--- a/sdmx2jsonld/transform/dimension.py
+++ b/sdmx2jsonld/transform/dimension.py
@@ -25,11 +25,11 @@
class Dimension(Property):
def __init__(self):
- super().__init__(entity='DimensionProperty')
- self.data['type'] = 'DimensionProperty'
+ super().__init__(entity="DimensionProperty")
+ self.data["type"] = "DimensionProperty"
- def add_data(self, id, data):
- super().add_data(id=id, data=data)
+ def add_data(self, property_id, data):
+ super().add_data(property_id=property_id, data=data)
# Add the id
- self.data['id'] = "urn:ngsi-ld:DimensionProperty:" + id
+ self.data["id"] = "urn:ngsi-ld:DimensionProperty:" + property_id
diff --git a/sdmx2jsonld/transform/distribution.py b/sdmx2jsonld/transform/distribution.py
new file mode 100644
index 0000000..8908c11
--- /dev/null
+++ b/sdmx2jsonld/transform/distribution.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from logging import getLogger
+from sdmx2jsonld.common.commonclass import CommonClass
+from json import load
+from random import getrandbits
+from os.path import dirname, join
+
+logger = getLogger()
+
+
+class Distribution(CommonClass):
+ def __init__(self):
+ super().__init__(entity="Distribution")
+ self.data = {
+ "id": "urn:ngsi-ld:Distribution:",
+ "type": "Distribution",
+ "accessUrl": {
+ "type": "Property",
+ "value": ["/ngsi-ld/v1/entities?type=https://smartdatamodels.org/dataModel.SDMX/Observation"],
+ },
+ "description": {
+ "type": "Property",
+ "value": "Distribution of statistical data observations.",
+ },
+ "format": {"type": "Property", "value": "JSON_LD"},
+ "language": {"type": "Property", "value": list()},
+ "status": {"type": "Property", "value": "Completed"},
+ "title": {"type": "Property", "value": list()},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
+ }
+
+ def generate_data(self, catalogue):
+ # Generate random id for the distribution
+ random_bits = getrandbits(128)
+ hash1 = "%032x" % random_bits
+ self.data["id"] += hash1
+
+ # Title is extracted from the dcterms:title from the Catalogue
+ self.data["title"]["value"] = catalogue.data["title"]["value"]
+
+ # language es obtained from language from the Catalogue
+ self.data["language"]["value"] = catalogue.data["language"]["value"]
+
+ # accessURL is generated from the configuration file.
+ config_path = dirname(dirname(dirname(__file__)))
+ config_path = join(join(config_path, "common"), "config.json")
+ with open(config_path) as config_file:
+ config = load(config_file)
+
+ self.data["accessUrl"]["value"][0] = config["broker"] + self.data["accessUrl"]["value"][0]
diff --git a/sdmx2jsonld/transform/entitytype.py b/sdmx2jsonld/transform/entitytype.py
index 16c861c..c790356 100644
--- a/sdmx2jsonld/transform/entitytype.py
+++ b/sdmx2jsonld/transform/entitytype.py
@@ -25,11 +25,16 @@
from sdmx2jsonld.transform.conceptschema import ConceptSchema
from sdmx2jsonld.transform.concept import Concept
from sdmx2jsonld.transform.attribute import Attribute
-from sdmx2jsonld.transform.catalogue import CatalogueDCATAP
+from sdmx2jsonld.transform.catalogue import Catalogue
+from sdmx2jsonld.transform.observation import Observation
from logging import getLogger
from datetime import datetime
from sdmx2jsonld.common.regparser import RegParser
-from sdmx2jsonld.common.classprecedence import Precedence, ClassesPrecedencePropertyError, ClassesPrecedenceClassError
+from sdmx2jsonld.common.classprecedence import (
+ Precedence,
+ ClassPrecedencePropertyError,
+ ClassPrecedenceClassError,
+)
logger = getLogger()
@@ -37,15 +42,17 @@
class EntityType:
def __init__(self):
self.entities = {
- 'qb:DataStructureDefinition': 'Dataset',
- 'qb:ComponentSpecification': 'Component',
- 'qb:AttributeProperty': 'Attribute',
- 'qb:DimensionProperty': 'Dimension',
- 'qb:CodedProperty': 'Dimension',
- 'rdfs:Class': 'Class',
- 'owl:Class': 'Class',
- 'skos:ConceptScheme': 'ConceptScheme',
- 'skos:Concept': 'Range'
+ "qb:DataSet": "Catalogue",
+ "qb:Observation": "Observation",
+ "qb:DataStructureDefinition": "Dataset",
+ "qb:ComponentSpecification": "Component",
+ "qb:AttributeProperty": "Attribute",
+ "qb:DimensionProperty": "Dimension",
+ "qb:CodedProperty": "Dimension",
+ "rdfs:Class": "Class",
+ "owl:Class": "Class",
+ "skos:ConceptScheme": "ConceptScheme",
+ "skos:Concept": "Range",
}
self.dataset = Dataset()
@@ -56,16 +63,16 @@ def __init__(self):
self.conceptListsIds = dict()
self.context = dict()
self.context_mapping = dict()
- self.catalogue = CatalogueDCATAP()
+ self.catalogue = Catalogue()
+ self.observations = list()
self.pre = Precedence()
+ self.parser = RegParser()
def __find_entity_type__(self, string):
"""
Find the index position of the 'a' SDMX key and return the following data with the corresponding EntityType
"""
- is_new = bool()
-
# Index maybe 0 in case of ComponentSpecification or 1 in case of DataStructureDefinition
index = len(string) - 1
string1 = string[index]
@@ -74,7 +81,8 @@ def __find_entity_type__(self, string):
# in case that there is no verb, we are talking about a triples whose id was previously
# created.
try:
- position = string1.index('a') + 1
+ position = string1.index("a") + 1
+ data = ""
try:
data = self.pre.precedence(string1[position])
@@ -82,22 +90,22 @@ def __find_entity_type__(self, string):
# We have two options, a well-know object list to be found in the self.entities or
# the conceptList defined in the turtle file
data = self.entities[data]
- except ClassesPrecedencePropertyError as error:
+ except ClassPrecedencePropertyError as error:
logger.error(str(error))
data = self.entities[data[0]]
- except ClassesPrecedenceClassError as error:
+ except ClassPrecedenceClassError as error:
logger.warning(str(error))
- data = self.entities['rdfs:Class']
+ data = self.entities["rdfs:Class"]
except KeyError:
# We found a CodeList or any other thing, check the list of codeList found in the turtle file
if data not in self.conceptListsIds:
logger.warning(f"Received a unexpected entity type: {data}")
else:
- data = 'Range'
+ data = "Range"
is_new = True
except ValueError:
- logger.info(f'Not a definition triples {string}, need to find the proper structure')
+ logger.info(f"Not a definition triples {string}, need to find the proper structure")
is_new = False
data = self.__get_subject__(title=string[0])
string1 = string[1:]
@@ -105,16 +113,12 @@ def __find_entity_type__(self, string):
return data, string1, is_new
def transform(self, string):
- if len(self.context) == 0:
- raise AssertionError("Context should be passed before to the EntityType Class, "
- "call EntityType.set_context() before, {'__file__': this_file}))")
-
data_type, new_string, is_new = self.__find_entity_type__(string=string)
if is_new:
- self.create_data(type=data_type, data=new_string, title=string[0])
+ self.create_data(entity_type=data_type, data=new_string, title=string[0])
else:
- logger.info(f'Checking previous subjects to find if it was created previously')
+ logger.info(f"Checking previous subjects to find if it was created previously")
self.patch_data(datatype=data_type, data=new_string)
def patch_data(self, datatype, data):
@@ -124,84 +128,105 @@ def flatten_value(y):
elif isinstance(y, datetime):
return y
else:
- return y.replace('"', '')
+ return y.replace('"', "")
flatten_data = [item for sublist in data for item in sublist]
- if flatten_data[0] != 'rdfs:label':
+ if flatten_data[0] != "rdfs:label":
flatten_data = {flatten_data[i]: flatten_value(flatten_data[i + 1]) for i in range(0, len(flatten_data), 2)}
language_map = False
else:
language_map = True
- if datatype == 'Dataset':
+ if datatype == "Dataset":
self.dataset.patch_data(data=flatten_data, language_map=language_map)
- def create_data(self, type, data, title):
- parser = RegParser()
-
- if type == 'Component':
- self.dataset.add_components(context=self.context, component=data)
- elif type == 'Dataset':
- identifier = parser.obtain_id(title)
- self.dataset.add_context(context=self.context, context_mapping=self.context_mapping)
+ def create_data(self, entity_type, data, title):
+ if entity_type == "Component":
+ (
+ some_new_component,
+ some_new_concept,
+ some_new_concept_schema,
+ ) = self.dataset.add_components(component=data)
+
+ if some_new_component is not None:
+ if some_new_component.data["type"] == "DimensionProperty":
+ # we have found special sdmx_dimensions that we have to add to dimensions list
+ self.dimensions.append(some_new_component)
+ elif some_new_component.data["type"] == "AttributeProperty":
+ # we have found special sdmx_attribute that we have to add to attributes list
+ self.attributes.append(some_new_component)
+ else:
+ # You should not be here, reporting error...
+ logger.error(
+ f'Unexpected entity type, id: {some_new_component.data["id"]} '
+ f'type: {some_new_component.data["type"]}'
+ )
+
+ if some_new_concept is not None:
+ self.conceptLists.append(some_new_concept)
+
+ # we need to check that the conceptSchema is not already defined in the structure
+ if some_new_concept_schema not in self.conceptSchemas:
+ self.conceptSchemas.append(some_new_concept_schema)
+ elif entity_type == "Catalogue":
+ identifier = self.parser.obtain_id(title)
+ self.catalogue.add_data(title=title, dataset_id=identifier, data=data)
+ elif entity_type == "Observation":
+ observation = Observation()
+ identifier = self.parser.obtain_id(title)
+ observation.add_data(title=title, observation_id=identifier, data=data)
+ self.observations.append(observation)
+ elif entity_type == "Dataset":
+ identifier = self.parser.obtain_id(title)
self.dataset.add_data(title=title, dataset_id=identifier, data=data)
# Create the CatalogueDCAT-AP and assign the dataset id
- self.catalogue.add_dataset(dataset_id=self.dataset.data['id'])
- elif type == 'Dimension':
+ self.catalogue.add_dataset(dataset_id=self.dataset.data["id"])
+ elif entity_type == "Dimension":
dimension = Dimension()
- dimension.add_context(context=self.context, context_mapping=self.context_mapping)
- dimension_id = parser.obtain_id(title)
- dimension.add_data(id=dimension_id, data=data)
+ dimension_id = self.parser.obtain_id(title)
+ dimension.add_data(property_id=dimension_id, data=data)
self.dimensions.append(dimension)
- elif type == 'Attribute':
+ elif entity_type == "Attribute":
attribute = Attribute()
- attribute.add_context(context=self.context, context_mapping=self.context_mapping)
- attribute_id = parser.obtain_id(title)
+ attribute_id = self.parser.obtain_id(title)
attribute.add_data(attribute_id=attribute_id, data=data)
self.attributes.append(attribute)
- elif type == 'ConceptScheme':
+ elif entity_type == "ConceptScheme":
concept_schema = ConceptSchema()
- concept_schema.add_context(context=self.context, context_mapping=self.context_mapping)
- concept_schema_id = parser.obtain_id(title)
+ concept_schema_id = self.parser.obtain_id(title)
concept_schema.add_data(concept_schema_id=concept_schema_id, data=data)
self.conceptSchemas.append(concept_schema)
- elif type == 'Class':
+ elif entity_type == "Class":
# We need the Concept because each of the Range description is of the type Concept
concept_list = Concept()
- concept_list.add_context(context=self.context, context_mapping=self.context_mapping)
- concept_list_id = parser.obtain_id(title)
+ concept_list_id = self.parser.obtain_id(title)
concept_list.add_data(concept_id=concept_list_id, data=data)
self.conceptLists.append(concept_list)
self.conceptListsIds[title] = concept_list.get_id()
- elif type == 'Range':
+ elif entity_type == "Range":
# TODO: Range is associated to a Concept and identified properly in the ConceptSchema
data_range = Concept()
- data_range.add_context(context=self.context, context_mapping=self.context_mapping)
- data_range_id = parser.obtain_id(title)
+ data_range_id = self.parser.obtain_id(title)
data_range.add_data(concept_id=data_range_id, data=data)
self.conceptLists.append(data_range)
self.conceptListsIds[title] = data_range.get_id()
-
- # for i in range(0, len(self.conceptSchemas)):
- # concept_schema = self.conceptSchemas[i].data
- # has_top_concept_values = concept_schema['skos:hasTopConcept']['value']
- #
- # out = [data_range.data['skos:notation']
- # if x == data_range.data['id'] else x for x in has_top_concept_values]
- #
- # self.conceptSchemas[i].data['skos:hasTopConcept']['value'] = out
+ else:
+ logger.error(f'Entity type "{entity_type}" not processed.')
def __get_subject__(self, title):
- if self.dataset.get()['dct:title'] == title:
- return 'Dataset'
+ if self.dataset.get()["dct:title"] == title:
+ return "Dataset"
else:
AssertionError(f"Still not defined: {title}")
def get_catalogue(self):
return self.catalogue.get()
+ def get_observation(self):
+ return self.observations
+
def get_dataset(self):
return self.dataset.get()
@@ -211,10 +236,10 @@ def get_dimensions(self):
def get_attributes(self):
return self.attributes
- def get_conceptSchemas(self):
+ def get_concept_schemas(self):
return self.conceptSchemas
- def get_conceptList(self):
+ def get_concept_list(self):
return self.conceptLists
def set_context(self, context, mapping):
diff --git a/sdmx2jsonld/transform/observation.py b/sdmx2jsonld/transform/observation.py
new file mode 100644
index 0000000..99493cf
--- /dev/null
+++ b/sdmx2jsonld/transform/observation.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+from logging import getLogger
+from sdmx2jsonld.common.commonclass import CommonClass
+from sdmx2jsonld.sdmxattributes.confirmationStatus import ConfStatus
+from sdmx2jsonld.sdmxattributes.observationStatus import ObsStatus
+from sdmx2jsonld.sdmxattributes.code import Code
+from sdmx2jsonld.sdmxdimensions.frequency import Frequency
+from sdmx2jsonld.common.regparser import RegParser
+from re import search, compile
+
+logger = getLogger()
+
+
+class Observation(CommonClass):
+ def __init__(self):
+ super().__init__(entity="Observation")
+
+ self.data = {
+ "id": str(),
+ "type": "Observation",
+ "title": {"type": "Property", "value": str()},
+ "identifier": {"type": "Property", "value": str()},
+ "dataSet": {"type": "Relationship", "object": str()},
+ "confStatus": {"type": "Property", "value": str()},
+ "decimals": {"type": "Property", "value": int()},
+ "obsStatus": {"type": "Property", "value": str()},
+ "unitMult": {"type": "Property", "value": int()},
+ "freq": {"type": "Property", "value": str()},
+ "refArea": {"type": "Property", "value": str()},
+ "timePeriod": {"type": "Property", "value": str()},
+ "obsValue": {"type": "Property", "value": float()},
+ "dimensions": {"type": "Property", "value": list()},
+ "@context": ["https://raw.githubusercontent.com/smart-data-models/dataModel.SDMX/master/context.jsonld"],
+ }
+
+ self.concept_id = str()
+ self.keys = {k: k for k in self.data.keys()}
+ self.regexpDimension = compile("ns1:.*")
+
+ def add_data(self, title, observation_id, data):
+ # We have a list of dimensions, a dataset, a list of attributes, a list of dimensions attributes
+ # and an observation
+
+ # Add the confStatus
+ key = self.__assign_property__(requested_key="sdmx-attribute:confStatus", data=data)
+ self.data[key]["value"] = ConfStatus().fix_value(value=self.data[key]["value"])
+
+ # Add the id
+ self.data["id"] = "urn:ngsi-ld:Observation:" + observation_id
+ self.data["identifier"]["value"] = observation_id
+
+ # Add title, the url string as it is in the turtle document
+ self.data["title"]["value"] = title
+
+ # Add the decimals
+ key = self.__assign_property__(requested_key="sdmx-attribute:decimals", data=data)
+ self.data[key]["value"] = Code(typecode=key).fix_value(value=self.data[key]["value"])
+
+ # Add obsStatus
+ key = self.__assign_property__(requested_key="sdmx-attribute:obsStatus", data=data)
+ self.data[key]["value"] = ObsStatus().fix_value(value=self.data[key]["value"])
+
+ # Add unitMult
+ key = self.__assign_property__(requested_key="sdmx-attribute:unitMult", data=data)
+ self.data[key]["value"] = Code(typecode=key).fix_value(value=self.data[key]["value"])
+
+ # Add freq "pattern": "^_[OUZ]|[SQBNI]|OA|OM|[AMWDH]_*[0-9]*$"
+ # TODO: Add verification of coded data following the pattern
+ key = self.__assign_property__(requested_key="sdmx-dimension:freq", data=data)
+ self.data[key]["value"] = Frequency().fix_value(value=self.data[key]["value"])
+
+ # Add reference Area
+ # TODO: Add verification of coded data following ISO-2, ISO-3, or M49 code
+ _ = self.__assign_property__(requested_key="sdmx-dimension:refArea", data=data)
+
+ # Add timePeriod
+ _ = self.__assign_property__(requested_key="sdmx-dimension:timePeriod", data=data)
+
+ # Add obsValue
+ _ = self.__assign_property__(requested_key="sdmx-measure:obsValue", data=data)
+
+ # Add dataset
+ parser = RegParser()
+ key = self.__assign_property__(requested_key="qb:dataSet", data=data, key_property="object")
+ identifier = parser.obtain_id(self.data[key]["object"])
+ self.data[key]["object"] = "urn:ngsi-ld:CatalogueDCAT-AP:" + identifier
+
+ # Add dimensions
+ result = self.__assign_dimensions__(data=data)
+ self.data["dimensions"]["value"] = result
+
+ def __assign_dimensions__(self, data):
+ def create_data(key, value):
+ new_dimension = {"key": key, "value": value}
+
+ return new_dimension
+
+ parser = RegParser()
+
+ # We need to get the list of available dimension keys
+ dimension_keys = [a for a in data if self.regexpDimension.match(a.__str__()) is not None]
+
+ # We need to get the list of values for these keys, if there is an url, it is considered a
+ values = [self.get_data(data[data.index(a) + 1]) for a in dimension_keys]
+ values = [parser.obtain_id(a, prefix_string="urn:ngsi-ld:Concept:") for a in values]
+
+ # Get the list of Entity IDs
+ entity_ids = ["urn:ngsi-ld:DimensionProperty:" + b.split("ns1:", 1)[1] for b in dimension_keys]
+
+ result = [create_data(key=entity_ids[i], value=values[i]) for i in range(0, len(values))]
+
+ return result
+
+ def __assign_property__(self, requested_key, data, key_property="value"):
+ key = self.get_key(requested_key=requested_key)
+ position = data.index(requested_key) + 1
+ self.data[key][key_property] = self.get_data(data[position])
+
+ return key
+
+ def get_key(self, requested_key):
+ try:
+ key = self.keys[requested_key]
+ return key
+ except KeyError:
+ # The key did not exist therefore we add to the list with this value
+ # We need to check if it exists without prefix
+ m = search("(.*):(.*)", requested_key)
+
+ if m is not None:
+ subfix = m.group(2)
+
+ try:
+ key = self.keys[subfix]
+ return key
+ except KeyError:
+ # Even subfix is not in the list, decide to add to the list of keys
+ self.keys[requested_key] = requested_key
+ return requested_key
+ else:
+ # Weird situation, it is an key without prefix and not recognised, decide to add it
+ self.keys[requested_key] = requested_key
+
+ return requested_key
+
+ def get(self):
+ return self.data
+
+ def get_data(self, data):
+ result = data
+ if isinstance(data, list):
+ result = self.get_data(data[0])
+
+ if isinstance(result, str):
+ m = search('"+(.*)"+', result)
+ if m is not None:
+ result = m.group(1)
+
+ return result
diff --git a/sdmx2jsonld/transform/parser.py b/sdmx2jsonld/transform/parser.py
index 2d5f3fb..c1becbe 100644
--- a/sdmx2jsonld/transform/parser.py
+++ b/sdmx2jsonld/transform/parser.py
@@ -29,10 +29,11 @@
from sdmx2jsonld.exceptions import UnexpectedEOF, UnexpectedInput, UnexpectedToken
from sdmx2jsonld.common.rdf import turtle_terse
from sdmx2jsonld.common.config import GRAMMARFILE
+from sdmx2jsonld.transform.distribution import Distribution
logger = getLogger(__name__)
-__version__ = "0.5.2"
+__version__ = "1.0.1"
class Parser:
@@ -41,7 +42,7 @@ def __init__(self):
with open(GRAMMARFILE) as f:
grammar = f.read()
- self.parser = Lark(grammar, start='start', parser='lalr')
+ self.parser = Lark(grammar, start="start", parser="lalr")
def parsing(self, content: TextIOBase, out: bool = False):
"""
@@ -62,11 +63,15 @@ def parsing(self, content: TextIOBase, out: bool = False):
def parsing_file(self, content: TextIOWrapper, out: bool):
transform = TreeToJson()
- content = content.read()
- content = turtle_terse(rdf_content=content)
+ with content as f:
+ data = f.read()
+
+ data = turtle_terse(rdf_content=data)
+ with open("./logs/final.ttl", "w") as outfile:
+ outfile.write(data)
try:
- tree = self.parser.parse(content)
+ tree = self.parser.parse(data)
except UnexpectedToken as err:
raise err
except UnexpectedInput as err:
@@ -78,16 +83,30 @@ def parsing_file(self, content: TextIOWrapper, out: bool):
if out:
# Save the generated content into files
- logger.info('Save the generated content into files')
+ logger.info("Save the generated content into files")
transform.save()
elif content is not None:
print()
- pprint(transform.get_catalogue())
- pprint(transform.get_dataset())
- [pprint(x.get()) for x in transform.get_dimensions()]
- [pprint(x.get()) for x in transform.get_attributes()]
- [pprint(x.get()) for x in transform.get_conceptSchemas()]
- [pprint(x.get()) for x in transform.get_conceptLists()]
+
+ catalogue = transform.get_catalogue()
+ pprint(catalogue)
+ ds = transform.get_dataset()
+ if ds is not None:
+ self.__check_pprint__(transform.get_dataset())
+ [pprint(x.get()) for x in transform.get_dimensions()] # type: ignore[func-returns-value]
+ [pprint(x.get()) for x in transform.get_attributes()] # type: ignore[func-returns-value]
+ [pprint(x.get()) for x in transform.get_concept_schemas()] # type: ignore[func-returns-value]
+ [pprint(x.get()) for x in transform.get_concept_lists()] # type: ignore[func-returns-value]
+
+ observations = transform.entity_type.get_observation()
+ if len(observations) != 0:
+ [pprint(x.get()) for x in observations] # type: ignore[func-returns-value]
+
+ # If we have several observations, we need to generate the DCAT-AP:Distribution class
+ distribution = Distribution()
+ distribution.generate_data(catalogue=transform.entity_type.catalogue)
+
+ pprint(distribution.get())
def parsing_string(self, content: StringIO):
transform = TreeToJson()
@@ -100,12 +119,32 @@ def parsing_string(self, content: StringIO):
# Serializing json payload
result = list()
- result.append(transform.get_catalogue())
- result.append(transform.get_dataset())
- [result.append(x.get()) for x in transform.get_dimensions()]
- [result.append(x.get()) for x in transform.get_attributes()]
- [result.append(x.get()) for x in transform.get_conceptSchemas()]
- [result.append(x.get()) for x in transform.get_conceptLists()]
+
+ catalogue = transform.get_catalogue()
+ result.append(catalogue)
+
+ ds = transform.get_dataset()
+ if ds is not None:
+ result.append(ds)
+
+ dimensions = transform.get_dimensions()
+ [result.append(x.get()) for x in dimensions] # type: ignore[func-returns-value]
+
+ [result.append(x.get()) for x in transform.get_attributes()] # type: ignore[func-returns-value]
+
+ [result.append(x.get()) for x in transform.get_concept_schemas()] # type: ignore[func-returns-value]
+
+ [result.append(x.get()) for x in transform.get_concept_lists()] # type: ignore[func-returns-value]
+
+ observations = transform.entity_type.get_observation()
+ if len(observations) != 0:
+ [result.append(x.get()) for x in observations] # type: ignore[func-returns-value]
+
+ # If we have several observations, we need to generate the DCAT-AP:Distribution class
+ distribution = Distribution()
+ # jicg. distribution.generate_data(catalogue=catalogue)
+ distribution.generate_data(catalogue=transform.entity_type.catalogue)
+ result.append(distribution.get())
json_object = dumps(result, indent=4, ensure_ascii=False)
@@ -113,3 +152,8 @@ def parsing_string(self, content: StringIO):
outfile.write(json_object)
return json_object
+
+ @staticmethod
+ def __check_pprint__(data):
+ if data is not None:
+ pprint(data)
diff --git a/sdmx2jsonld/transform/property.py b/sdmx2jsonld/transform/property.py
index 5682f44..f233685 100644
--- a/sdmx2jsonld/transform/property.py
+++ b/sdmx2jsonld/transform/property.py
@@ -35,60 +35,46 @@ def __init__(self, entity):
self.data = {
"id": str(),
"type": "",
- "dct:language": {
- "type": "Property",
- "value": list()
- },
-
+ "language": {"type": "Property", "value": list()},
#################################################
# TODO: New ETSI CIM NGSI-LD specification 1.4.2
# Pending to implement in the Context Broker
#################################################
- # "rdfs:label": {
+ # "label": {
# "type": "LanguageProperty",
# "LanguageMap": dict()
# },
#################################################
- "rdfs:label": {
- "type": "Property",
- "value": dict()
- },
-
-
- "qb:codeList": {
- "type": "Relationship",
- "object": str()
- },
- "qb:concept": {
- "type": "Property",
- "value": str()
- },
- "@context": dict()
+ "label": {"type": "Property", "value": dict()},
+ "codeList": {"type": "Relationship", "object": str()},
+ "concept": {"type": "Relationship", "object": str()},
+ "@context": [
+ "https://raw.githubusercontent.com/smart-data-models/dataModel.STAT-DCAT-AP/master/context.jsonld"
+ ],
}
self.keys = {k: k for k in self.data.keys()}
- def add_data(self, id, data):
+ def add_data(self, property_id, data):
# TODO: We have to control that data include the indexes that we want to search
# We need to complete the data corresponding to the Dimension: rdfs:label
- position = data.index('rdfs:label') + 1
+ position = data.index("rdfs:label") + 1
description = data[position]
- descriptions = [x[0].replace("\"", "") for x in description]
+ descriptions = [x[0].replace('"', "") for x in description]
languages = list()
try:
languages = [x[1].replace("@", "").lower() for x in description]
except IndexError:
- logger.warning(f'The Property {id} has a '
- f'rdfs:label without language tag: {description}')
+ logger.warning(f"The Property {property_id} has a " f"rdfs:label without language tag: {description}")
aux = len(description)
if aux != 1:
logger.error(f"Property: there is more than 1 description ({aux}), values: {description}")
else:
# There is no language tag, we use by default 'en'
- languages = ['en']
+ languages = ["en"]
logger.warning('Property: selecting default language "en"')
###############################################################################
@@ -96,57 +82,55 @@ def add_data(self, id, data):
# Pending to implement in the Context Broker
###############################################################################
# for i in range(0, len(languages)):
- # self.data['rdfs:label']['LanguageMap'][languages[i]] = descriptions[i]
+ # self.data['label']['LanguageMap'][languages[i]] = descriptions[i]
###############################################################################
for i in range(0, len(languages)):
- self.data['rdfs:label']['value'][languages[i]] = descriptions[i]
+ self.data["label"]["value"][languages[i]] = descriptions[i]
# Complete the information of the language with the previous information
- key = self.keys['dct:language']
- self.data[key]['value'] = languages
+ key = self.keys["language"]
+ self.data[key]["value"] = languages
# qb:codeList, this attribute might not be presented, so we need to check it.
# TODO: We need to control that the codeList id extracted here are the same that we analyse afterwards.
try:
- position = data.index('qb:codeList') + 1
- code_list = self.generate_id(entity="ConceptSchema", value=data[position][0])
- self.data['qb:codeList']['object'] = code_list
+ position = data.index("qb:codeList") + 1
+ code_list, uri = self.generate_id(entity="ConceptSchema", value=data[position][0])
+ self.data["codeList"]["object"] = uri
except ValueError:
- logger.warning(f'Property: {id} has not qb:codeList, deleting the key in the data')
+ logger.warning(f"Property: {property_id} has not qb:codeList, deleting the key in the data")
# If we have not the property, we delete it from data
- self.data.pop('qb:codeList')
+ self.data.pop("codeList")
# qb:concept
# TODO: the concept id need to check if it is a normal id or an url
- position = data.index('qb:concept') + 1
- concept = self.generate_id(entity="Concept", value=data[position][0])
- self.data['qb:concept']['value'] = concept
+ position = data.index("qb:concept") + 1
+ concept, uri = self.generate_id(entity="Concept", value=data[position][0])
+ self.data["concept"]["object"] = uri
# Get the rest of the data
- data = get_rest_data(data=data,
- not_allowed_keys=[
- 'sliceKey',
- 'component',
- 'disseminationStatus',
- 'validationState',
- 'notation',
- 'label',
- 'codeList',
- 'concept'
- ],
- further_process_keys=[
- 'component',
- 'label'
- ])
+ data = get_rest_data(
+ data=data,
+ not_allowed_keys=[
+ "sliceKey",
+ "component",
+ "disseminationStatus",
+ "validationState",
+ "notation",
+ "label",
+ "codeList",
+ "concept",
+ ],
+ further_process_keys=["component", "label"],
+ )
# add the new data to the dataset structure
- [self.data.update({k: v}) for k, v in data.items()]
+ [self.data.update(self.__generate_property__(key=k, value=v)) for k, v in data.items()]
- # Simplify Context and order keys
+ # Order the keys in the final json-ld
a = Context()
- a.set_data(data=self.data)
- a.new_analysis()
+ a.set_data(new_data=self.data)
a.order_context()
self.data = a.get_data()
diff --git a/sdmx2jsonld/transform/transformer.py b/sdmx2jsonld/transform/transformer.py
index b20e70d..021ba8a 100644
--- a/sdmx2jsonld/transform/transformer.py
+++ b/sdmx2jsonld/transform/transformer.py
@@ -20,10 +20,11 @@
# under the License.
##
-from lark import Transformer, Tree, Token
+from lark import Transformer
from sdmx2jsonld.transform.context import Context
from sdmx2jsonld.transform.entitytype import EntityType
from sdmx2jsonld.common.datatypeconversion import DataTypeConversion
+from sdmx2jsonld.transform.distribution import Distribution
import re
@@ -34,7 +35,6 @@ def __init__(self):
self.entity_type = EntityType()
# Regex to check valid URL
- # regex = ""
regex = "http[s]?:\/\/(.*)"
# Compile the Regex
@@ -46,12 +46,11 @@ def prefixid(self, s):
self.context.add_context(context)
def triples(self, triple):
- self.entity_type.set_context(context=self.get_context(), mapping=self.get_context_mapping())
self.entity_type.transform(string=triple)
return triple
def predicate(self, pre):
- result = ''
+ result = ""
if isinstance(pre[0], str):
result = pre[0]
else:
@@ -93,8 +92,8 @@ def iri(self, iri):
def verb(self, verb):
return str(verb[0])
- def object(self, object):
- return object[0]
+ def object(self, my_object):
+ return my_object[0]
def literal(self, literal):
return literal[0]
@@ -115,8 +114,14 @@ def get_context_mapping(self):
def get_catalogue(self):
return self.entity_type.get_catalogue()
+ def get_observation(self):
+ if self.entity_type.observations.data["id"] != "":
+ return self.entity_type.get_observation()
+
def get_dataset(self):
- return self.entity_type.get_dataset()
+ if self.entity_type.dataset.data["id"] != "":
+ return self.entity_type.get_dataset()
+ return None
def get_dimensions(self):
return self.entity_type.get_dimensions()
@@ -124,16 +129,17 @@ def get_dimensions(self):
def get_attributes(self):
return self.entity_type.get_attributes()
- def get_conceptSchemas(self):
- return self.entity_type.get_conceptSchemas()
+ def get_concept_schemas(self):
+ return self.entity_type.get_concept_schemas()
- def get_conceptLists(self):
- return self.entity_type.get_conceptList()
+ def get_concept_lists(self):
+ return self.entity_type.get_concept_list()
def save(self):
- self.entity_type.save('catalogue')
+ self.entity_type.save("catalogue")
- self.entity_type.save('dataset')
+ if self.entity_type.dataset.data["id"] != "":
+ self.entity_type.save("dataset")
dimensions = self.entity_type.get_dimensions()
[dimension.save() for dimension in dimensions]
@@ -141,9 +147,18 @@ def save(self):
attributes = self.entity_type.get_attributes()
[attribute.save() for attribute in attributes]
- concept_schemas = self.entity_type.get_conceptSchemas()
+ concept_schemas = self.entity_type.get_concept_schemas()
[x.save() for x in concept_schemas]
- concept_lists = self.entity_type.get_conceptList()
+ concept_lists = self.entity_type.get_concept_list()
[x.save() for x in concept_lists]
+ if len(self.entity_type.observations) != 0:
+ observations = self.entity_type.get_observation()
+ [observation.save() for observation in observations]
+
+ # If we have several observations, we need to generate the DCAT-AP:Distribution class
+ distribution = Distribution()
+ distribution.generate_data(catalogue=self.entity_type.catalogue)
+
+ distribution.save()
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..32f1147
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,9 @@
+# python3.11, virtualenv -ppython3.11 .venv
+# file with the requirements to execute the python tests using tox
+pytest==7.4.0
+tox==4.6.4
+types-docopt==0.6.11.3
+types-python-dateutil==2.8.19.14
+types-pytz==2023.3.0.0
+types-requests==2.31.0.2
+coverage==7.2.7
diff --git a/tests/.env b/tests/.env
new file mode 100755
index 0000000..a9fa8f7
--- /dev/null
+++ b/tests/.env
@@ -0,0 +1,10 @@
+# Project name
+COMPOSE_PROJECT_NAME=fiware
+
+# Orion variables
+ORION_LD_PORT=1026
+ORION_LD_VERSION=1.0.0
+
+# MongoDB variables
+MONGO_DB_PORT=27017
+MONGO_DB_VERSION=4.4
diff --git a/tests/common/test_classprecedence.py b/tests/common/test_classprecedence.py
index 974b3cf..fcaab52 100644
--- a/tests/common/test_classprecedence.py
+++ b/tests/common/test_classprecedence.py
@@ -20,7 +20,11 @@
# under the License.
##
from unittest import TestCase
-from sdmx2jsonld.common.classprecedence import Precedence, ClassesPrecedencePropertyError, ClassesPrecedenceClassError
+from sdmx2jsonld.common.classprecedence import (
+ Precedence,
+ ClassPrecedencePropertyError,
+ ClassPrecedenceClassError,
+)
class Test(TestCase):
@@ -31,44 +35,44 @@ def test_precedence_one_class(self):
"""
The precedence of one Class will be ALWAYS that Class
"""
- obtained = self.pre.precedence(['qb:DataStructureDefinition'])
- expected = 'qb:DataStructureDefinition'
+ obtained = self.pre.precedence(["qb:DataStructureDefinition"])
+ expected = "qb:DataStructureDefinition"
assert obtained == expected, f"'qb:DataStructureDefinition' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['skos:Concept'])
- expected = 'skos:Concept'
+ obtained = self.pre.precedence(["skos:Concept"])
+ expected = "skos:Concept"
assert obtained == expected, f"'skos:Concept' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:SliceKey'])
- expected = 'qb:SliceKey'
+ obtained = self.pre.precedence(["qb:SliceKey"])
+ expected = "qb:SliceKey"
assert obtained == expected, f"'qb:SliceKey' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:DimensionProperty'])
- expected = 'qb:DimensionProperty'
+ obtained = self.pre.precedence(["qb:DimensionProperty"])
+ expected = "qb:DimensionProperty"
assert obtained == expected, f"'qb:DimensionProperty' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:AttributeProperty'])
- expected = 'qb:AttributeProperty'
+ obtained = self.pre.precedence(["qb:AttributeProperty"])
+ expected = "qb:AttributeProperty"
assert obtained == expected, f"'qb:AttributeProperty' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['skos:ConceptScheme'])
- expected = 'skos:ConceptScheme'
+ obtained = self.pre.precedence(["skos:ConceptScheme"])
+ expected = "skos:ConceptScheme"
assert obtained == expected, f"'skos:ConceptScheme' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['owl:Class'])
- expected = 'owl:Class'
+ obtained = self.pre.precedence(["owl:Class"])
+ expected = "owl:Class"
assert obtained == expected, f"'owl:Class' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:ComponentSpecification'])
- expected = 'qb:ComponentSpecification'
+ obtained = self.pre.precedence(["qb:ComponentSpecification"])
+ expected = "qb:ComponentSpecification"
assert obtained == expected, f"'qb:ComponentSpecification' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:MeasureProperty'])
- expected = 'qb:MeasureProperty'
+ obtained = self.pre.precedence(["qb:MeasureProperty"])
+ expected = "qb:MeasureProperty"
assert obtained == expected, f"'qb:MeasureProperty' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['skos:ConceptScheme'])
- expected = 'skos:ConceptScheme'
+ obtained = self.pre.precedence(["skos:ConceptScheme"])
+ expected = "skos:ConceptScheme"
assert obtained == expected, f"'skos:ConceptScheme' expected, got: '{obtained}'"
def test_precedence_classes_with_dimension_and_attribute_values(self):
@@ -78,55 +82,64 @@ def test_precedence_classes_with_dimension_and_attribute_values(self):
different from this, therefore we should return an error)
2) "rdfs:Class", "owl:Class"
"""
- with self.assertRaises(ClassesPrecedencePropertyError) as error:
+ with self.assertRaises(ClassPrecedencePropertyError) as error:
_ = self.pre.precedence(["qb:DimensionProperty", "qb:AttributeProperty"])
- self.assertEqual(str(error.exception),
- "['qb:DimensionProperty', 'qb:AttributeProperty'] -> Incompatible multiclass definition")
+ self.assertEqual(
+ str(error.exception),
+ "['qb:DimensionProperty', 'qb:AttributeProperty'] -> Incompatible multiclass definition",
+ )
def test_precedence_classes_with_attribute_and_measure_values(self):
- with self.assertRaises(ClassesPrecedencePropertyError) as error:
+ with self.assertRaises(ClassPrecedencePropertyError) as error:
_ = self.pre.precedence(["qb:AttributeProperty", "qb:MeasureProperty"])
- self.assertEqual(str(error.exception),
- "['qb:AttributeProperty', 'qb:MeasureProperty'] -> Incompatible multiclass definition")
+ self.assertEqual(
+ str(error.exception),
+ "['qb:AttributeProperty', 'qb:MeasureProperty'] -> Incompatible multiclass definition",
+ )
def test_precedence_classes_with_dimension_and_measure_values(self):
- with self.assertRaises(ClassesPrecedencePropertyError) as error:
+ with self.assertRaises(ClassPrecedencePropertyError) as error:
_ = self.pre.precedence(["qb:DimensionProperty", "qb:MeasureProperty"])
- self.assertEqual(str(error.exception),
- "['qb:DimensionProperty', 'qb:MeasureProperty'] -> Incompatible multiclass definition")
+ self.assertEqual(
+ str(error.exception),
+ "['qb:DimensionProperty', 'qb:MeasureProperty'] -> Incompatible multiclass definition",
+ )
def test_precedence_classes_with_class_values(self):
- with self.assertRaises(ClassesPrecedenceClassError) as error:
+ with self.assertRaises(ClassPrecedenceClassError) as error:
_ = self.pre.precedence(["rdfs:Class", "owl:Class"])
- self.assertEqual(str(error.exception), "['rdfs:Class', 'owl:Class'] -> Possible redundant Class definition")
+ self.assertEqual(
+ str(error.exception),
+ "['rdfs:Class', 'owl:Class'] -> Possible redundant Class definition",
+ )
def test_attribute_and_coded_property(self):
- obtained = self.pre.precedence(['qb:AttributeProperty', 'qb:CodedProperty'])
- expected = 'qb:AttributeProperty'
+ obtained = self.pre.precedence(["qb:AttributeProperty", "qb:CodedProperty"])
+ expected = "qb:AttributeProperty"
assert obtained == expected, f"'qb:AttributeProperty' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:CodedProperty', 'qb:AttributeProperty'])
- expected = 'qb:AttributeProperty'
+ obtained = self.pre.precedence(["qb:CodedProperty", "qb:AttributeProperty"])
+ expected = "qb:AttributeProperty"
assert obtained == expected, f"'qb:AttributeProperty' expected, got: '{obtained}'"
def test_coded_and_dimension_property(self):
- obtained = self.pre.precedence(['qb:CodedProperty', 'qb:DimensionProperty'])
- expected = 'qb:DimensionProperty'
+ obtained = self.pre.precedence(["qb:CodedProperty", "qb:DimensionProperty"])
+ expected = "qb:DimensionProperty"
assert obtained == expected, f"'qb:DimensionProperty' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['qb:DimensionProperty', 'qb:CodedProperty'])
- expected = 'qb:DimensionProperty'
+ obtained = self.pre.precedence(["qb:DimensionProperty", "qb:CodedProperty"])
+ expected = "qb:DimensionProperty"
assert obtained == expected, f"'qb:DimensionProperty' expected, got: '{obtained}'"
def test_concept_and_other_property(self):
- obtained = self.pre.precedence(['skos:Concept', ''])
- expected = 'skos:Concept'
+ obtained = self.pre.precedence(["skos:Concept", ""])
+ expected = "skos:Concept"
assert obtained == expected, f"'skos:Concept' expected, got: '{obtained}'"
- obtained = self.pre.precedence(['', 'skos:Concept'])
- expected = 'skos:Concept'
+ obtained = self.pre.precedence(["", "skos:Concept"])
+ expected = "skos:Concept"
assert obtained == expected, f"'skos:Concept' expected, got: '{obtained}'"
diff --git a/tests/common/test_commonclass.py b/tests/common/test_commonclass.py
new file mode 100644
index 0000000..e5d9479
--- /dev/null
+++ b/tests/common/test_commonclass.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+from unittest import TestCase
+from sdmx2jsonld.common.commonclass import CommonClass
+import os
+import json
+
+
+class TestCommonClass(TestCase):
+ def setUp(self) -> None:
+ pass
+
+ def test_instance_class(self):
+ cclass = CommonClass("test.common.entity")
+ urnid = cclass.generate_id("https://string-to-parse-ur/entity_id", update_id=True)
+ assert urnid == "urn:ngsi-ld:test.common.entity:entity_id"
+ # urnid = cclass.generate_id("")
+ # print(urnid)
+
+ def test_save(self):
+ context = {
+ "@context": "https://raw.githubusercontent.com/smart-data-models/data-models/master/context/merge_subjects_config_example.json"
+ }
+ context_map = {
+ "address": "https://smartdatamodels.org/address",
+ "alternateName": "https://smartdatamodels.org/alternateName",
+ "status": "ngsi-ld:status",
+ }
+ cclass = CommonClass("test.common.entity")
+ urnid = cclass.generate_id("https://string-to-parse-ur/entity_id", update_id=True)
+ assert urnid == "urn:ngsi-ld:test.common.entity:entity_id"
+
+ cclass.add_context(context, context_map)
+
+ os.makedirs("/tmp/commonclass", exist_ok=True)
+ os.chdir("/tmp/commonclass")
+ cclass.save()
+
+ with open("/tmp/commonclass/output/test.common.entity_entity_id.jsonld", "r") as f:
+ data = json.load(f)
+ assert data["id"] == urnid
+ assert data["@context"] == context["@context"]
+
+ # TODO - Add tests with cclass.generate_id using update_id with a False value
diff --git a/tests/common/test_datatypeconversion.py b/tests/common/test_datatypeconversion.py
new file mode 100755
index 0000000..b3b2340
--- /dev/null
+++ b/tests/common/test_datatypeconversion.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.common.datatypeconversion import DataTypeConversion
+import sys
+
+
+class TestDataTypeConversion(TestCase):
+ def setUp(self) -> None:
+ pass
+
+ def test_string_to_integer(self):
+ dtc = DataTypeConversion()
+ token_type = "xsd:int"
+ # token = Token('FORMATCONNECTOR', "432112")
+
+ i: int = dtc.convert("432112", token_type)
+ assert i == 432112
+
+ i = dtc.convert("-100", token_type)
+ assert i == -100
+
+ i = dtc.convert("19223372036854775808", token_type)
+ assert i == 19223372036854775808
+ assert type(i) is int
+
+ try:
+ dtc.convert("invalid value", token_type)
+ assert False
+ except Exception:
+ assert True
+
+ def test_string_to_bool(self):
+ dtc = DataTypeConversion()
+ token_type = "xsd:boolean"
+
+ values = ("True", "true", "y", "yes", "T", "1", 1, True)
+ for value in values:
+ assert dtc.convert(f'"{value}"', token_type)
+
+ values = (
+ "fAlsE",
+ "False",
+ "N",
+ "No",
+ "F",
+ "0",
+ "0.0",
+ "",
+ "None",
+ None,
+ [],
+ {},
+ 0,
+ 0.0,
+ )
+ for value in values:
+ assert not dtc.convert(f'"{value}"', token_type)
+
+ invalid_values = (5, 4.2, "invalid value", "nil")
+ for value in invalid_values:
+ try:
+ dtc.convert(value, token_type)
+ assert False
+ except Exception:
+ assert True
+
+ def test_string_to_dates(self):
+ dtc = DataTypeConversion()
+ token_type = "xsd:dateTime"
+
+ dates = (
+ '"2022-01-15T08:00:00.000+00:00"',
+ '"2022-01-10T09:00:00.000"',
+ '"2021-07-01T11:50:37.3"',
+ '"2021-09-28T15:31:24.05"',
+ '"Mon Jan 13 09:52:52 MST 2014"',
+ '"Thu Jun 02 11:56:53 CDT 2011"',
+ '"2022-12-12T10:00:00"',
+ '"2022-05-11T10:00:00"',
+ '"Tue Dec 13 11:00:00 K 2022"',
+ '"2021-07-01T11:58:08.642000"',
+ )
+ expected = (
+ "2022-01-15T08:00:00+00:00",
+ "2022-01-10T08:00:00+00:00",
+ "2021-07-01T09:50:37.300000+00:00",
+ "2021-09-28T13:31:24.050000+00:00",
+ "2014-01-13T16:52:52+00:00",
+ "2011-06-02T16:56:53+00:00",
+ "2022-12-12T09:00:00+00:00",
+ "2022-05-11T08:00:00+00:00",
+ "2022-12-13T01:00:00+00:00",
+ "2021-07-01T09:58:08.642000+00:00",
+ )
+ # "2021-07-01T09:58:08.642000+00:00"
+ d = zip(dates, expected)
+
+ for test_date, expected_date in d:
+ assert expected_date == dtc.convert(test_date, token_type)
+
+
+class Test(TestCase):
+ def setUp(self):
+ self.conversion = DataTypeConversion()
+
+ def test_datetime_string_conversion_1(self):
+ """
+ Check if we can get a correct datetime value from a string, case 1
+ """
+ obtained = self.conversion.convert('"2022-01-15T08:00:00.000 UTC"', "xsd:dateTime")
+ expected = "2022-01-15T08:00:00+00:00"
+ assert obtained == expected, (
+ f"\n\nDateTime was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_int_string_conversion(self):
+ """
+ Check if we can get a correct integer from a string
+ """
+ obtained = self.conversion.convert('"2"', "xsd:int")
+ expected = 2
+ assert obtained == expected, (
+ f"\n\nInteger was not the expected,"
+ f"\n got : {obtained} {type(obtained)}"
+ f"\n expected: {expected} {type(expected)}"
+ )
+
+ def test_int_integer_conversion(self):
+ """
+ Check if we can get a correct integer from an integer
+ """
+ obtained = self.conversion.convert("2", "xsd:int")
+ expected = 2
+ assert obtained == expected, (
+ f"\n\nInteger was not the expected,"
+ f"\n got : {obtained} {type(obtained)}"
+ f"\n expected: {expected} {type(expected)}"
+ )
+
+ # # print(dataConversionType.convert(data23[0], data23[2]) + 10)
+ #
+
+ def test_boolean_conversion(self):
+ """
+ Check if we can convert a boolean string into its proper value
+ """
+ obtained = self.conversion.convert('"true"', "xsd:boolean")
+ expected = True
+ assert obtained == expected, (
+ f"\n\nBoolean was not the expected,"
+ f"\n got : {obtained} {type(obtained)}"
+ f"\n expected: {expected} {type(expected)}"
+ )
+
+ def test_fake_conversion(self):
+ """
+ Check is a fake value data launch an exception
+ """
+ with self.assertRaises(Exception) as error:
+ _ = self.conversion.convert('"fake"', "otraCosa")
+
+ self.assertEqual(str(error.exception), "Datatype not defined: otraCosa")
+
+ # # Convert datetime generated into UTC format: 2021-12-21T16:18:55Z or 2021-12-21T16:18:55+00:00, ISO8601
+ #
+ # data5 = ['"2022-01-10T09:00:00.000"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
+ # print(dataConversionType.convert(data5[0], data5[2]))
+ #
+ # data6 = ['"2021-07-01T11:50:37.3"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
+ # print(dataConversionType.convert(data6[0], data6[2]))
+ #
+ # data7 = ['"2021-09-28T15:31:24.05"', Token('FORMATCONNECTOR', '^^'), 'xsd:dateTime']
+ # print(dataConversionType.convert(data7[0], data7[2]))
+ #
+
+ def test_float_float_conversion(self):
+ """
+ Check if we can get a correct integer from an string
+ """
+ obtained = self.conversion.convert("2345.2", "xsd:float")
+ expected = 2345.2
+ assert obtained == expected, (
+ f"\n\nInteger was not the expected,"
+ f"\n got : {obtained} {type(obtained)}"
+ f"\n expected: {expected} {type(expected)}"
+ )
+
+ def test_float_string_conversion(self):
+ """
+ Check if we can get a correct integer from an integer
+ """
+ obtained = self.conversion.convert('"3016.9"', "xsd:float")
+ expected = 3016.9
+ assert obtained == expected, (
+ f"\n\nFloat was not the expected,"
+ f"\n got : {obtained} {type(obtained)}"
+ f"\n expected: {expected} {type(expected)}"
+ )
diff --git a/tests/common/test_listmanagement.py b/tests/common/test_listmanagement.py
new file mode 100644
index 0000000..58f7e10
--- /dev/null
+++ b/tests/common/test_listmanagement.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.common.listmanagement import (
+ get_rest_data,
+ flatten_value,
+ extract_prefix,
+ get_property_value,
+)
+from sdmx2jsonld.exceptions.exceptions import ClassExtractPrefixError
+
+
+class TestRegToParser(TestCase):
+ def setUp(self) -> None:
+ pass
+
+ def test_get_rest_data(self):
+ data = [
+ "a",
+ ["qb:AttributeProperty"],
+ "rdfs:label",
+ [['"SDMX attribute COMMENT_OBS"', "@en"], ['"Attribut SDMX "', "@fr"]],
+ "dct:created",
+ [["2022-01-15T06:00:00+00:00"]],
+ "dct:identifier",
+ [['"a3003"']],
+ "dct:modified",
+ [["2022-01-15T06:30:00+00:00"]],
+ "qb:concept",
+ ["http://bauhaus/concepts/definition/c4303"],
+ "insee:disseminationStatus",
+ ["http://id.insee.fr/codes/base/statutDiffusion/Prive"],
+ "insee:validationState",
+ [['"Unpublished"']],
+ "rdfs:range",
+ ["xsd:string"],
+ "skos:notation",
+ [['"COMMENT_OBS"']],
+ ]
+ not_allowed_keys = [
+ "sliceKey",
+ "component",
+ "disseminationStatus",
+ "validationState",
+ "notation",
+ "label",
+ "codeList",
+ "concept",
+ ]
+ further_process_keys = ["component", "label"]
+ expected_res = {
+ "created": "2022-01-15T06:00:00+00:00",
+ "identifier": "a3003",
+ "modified": "2022-01-15T06:30:00+00:00",
+ "range": "xsd:string",
+ }
+ res = get_rest_data(data, not_allowed_keys, further_process_keys)
+ assert expected_res == res
+
+ def test_flatten_value(self):
+ data = [
+ ['"SDMX attribute PRE_BREAK_VALUE"', "@en"],
+ ['"Attribut SDMX "', "@fr"],
+ ]
+ expected_res = {"en": "SDMX attribute PRE_BREAK_VALUE", "fr": "Attribut SDMX "}
+ got_data = flatten_value(data)
+ assert got_data == expected_res
+
+ # data = ['', None, 'dct:created', 'dct:identifier', 'dct:modified', 'rdfs:range', 'uno', 'a:b:c']
+ def test_extract_prefix_with_a_prefix(self):
+ data = "a:b"
+ expected_res = "b"
+ got_res = extract_prefix(data)
+ assert got_res == expected_res
+
+ def test_extract_prefix_with_several_prefixes(self):
+ data = "a:b:c"
+ expected = "Unexpected number of prefixes: 'a:b:c'"
+ with self.assertRaises(ClassExtractPrefixError) as error:
+ _ = extract_prefix(attribute=data)
+
+ self.assertEqual(str(error.exception.message), expected)
+
+ def test_extract_prefix_with_None_value(self):
+ data = None
+ expected = "Unexpected data received: 'None'"
+ with self.assertRaises(ClassExtractPrefixError) as error:
+ _ = extract_prefix(attribute=data)
+
+ self.assertEqual(str(error.exception.message), expected)
+
+ def test_extract_prefix_with_empty_value(self):
+ data = ""
+ expected = "Unexpected data received: ''"
+ with self.assertRaises(ClassExtractPrefixError) as error:
+ _ = extract_prefix(attribute=data)
+
+ self.assertEqual(str(error.exception.message), expected)
+
+ def test_extract_prefix_with_a_value_without_prefix(self):
+ data = "a"
+ expected_res = "a"
+ got_res = extract_prefix(data)
+ assert got_res == expected_res
+
+ def test_get_property_data_from_array_property_without_prefix(self):
+ data = [
+ "a",
+ ["qb:DataSet"],
+ "rdfs:label",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "dcterms:issued",
+ [["2022-04-01T06:00:00+00:00"]],
+ "dcterms:publisher",
+ ["http://id.insee.fr/organisations/insee"],
+ "dcterms:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "qb:structure",
+ ["http://bauhaus/structuresDeDonnees/structure/dsd3001"],
+ "sdmx-attribute:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ ]
+ expected = [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ]
+
+ index, key, obtained = get_property_value(data=data, property_name="title")
+
+ self.assertEqual(index, 8)
+ self.assertEqual(key, "dcterms:title")
+ self.assertEqual(expected, obtained)
+
+ def test_get_property_data_from_array_property_with_prefix(self):
+ data = [
+ "a",
+ ["qb:DataSet"],
+ "rdfs:label",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "dcterms:issued",
+ [["2022-04-01T06:00:00+00:00"]],
+ "dcterms:publisher",
+ ["http://id.insee.fr/organisations/insee"],
+ "dcterms:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "qb:structure",
+ ["http://bauhaus/structuresDeDonnees/structure/dsd3001"],
+ "sdmx-attribute:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ ]
+ expected = [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ]
+
+ index, key, obtained = get_property_value(data=data, property_name="dcterms:title")
+
+ self.assertEqual(index, 8)
+ self.assertEqual(key, "dcterms:title")
+ self.assertEqual(expected, obtained)
+
+ def test_get_property_data_from_array_invalid_property(self):
+ data = [
+ "a",
+ ["qb:DataSet"],
+ "rdfs:label",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "dcterms:issued",
+ [["2022-04-01T06:00:00+00:00"]],
+ "dcterms:publisher",
+ ["http://id.insee.fr/organisations/insee"],
+ "dcterms:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ "qb:structure",
+ ["http://bauhaus/structuresDeDonnees/structure/dsd3001"],
+ "sdmx-attribute:title",
+ [
+ ['"GDP and main components (current prices)"', "@en"],
+ ['"PIB et principales composantes (prix courants)"', "@fr"],
+ ],
+ ]
+ expected = ""
+
+ index, key, obtained = get_property_value(data=data, property_name="any")
+
+ self.assertEqual(index, -1)
+ self.assertEqual(key, "")
+ self.assertEqual(expected, obtained)
diff --git a/tests/common/test_rdf.py b/tests/common/test_rdf.py
new file mode 100644
index 0000000..25145df
--- /dev/null
+++ b/tests/common/test_rdf.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.common.rdf import turtle_terse
+from rdflib import Graph
+
+
+class TestRegToParser(TestCase):
+ def setUp(self) -> None:
+ pass
+
+ def test_turtle_1(self):
+ rdf_data = """
+@prefix ab: .
+
+ab:richard ab:homeTel "(229) 276-5135" .
+ab:richard ab:email "richard49@hotmail.com" .
+
+ab:cindy ab:homeTel "(245) 646-5488" .
+ab:cindy ab:email "cindym@gmail.com" .
+
+ab:craig ab:homeTel "(194) 966-1505" .
+ab:craig ab:email "craigellis@yahoo.com" .
+ab:craig ab:email "c.ellis@usairwaysgroup.com" .
+ """
+ rdf_content = turtle_terse(rdf_data)
+
+ gx = Graph()
+ gx = gx.parse(data=rdf_content, format="turtle")
+ ser = gx.serialize(format="turtle")
+
+ assert rdf_content == ser
diff --git a/tests/common/test_regparser.py b/tests/common/test_regparser.py
new file mode 100644
index 0000000..589c859
--- /dev/null
+++ b/tests/common/test_regparser.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.common.regparser import RegParser
+
+
+class TestRegToParser(TestCase):
+ def setUp(self) -> None:
+ pass
+
+ def test_get_id(self):
+ re = RegParser()
+ assert re.obtain_id("https://elmundo.es/episode-one") == "episode-one"
diff --git a/tests/files/observations.ttl b/tests/files/observations.ttl
new file mode 100644
index 0000000..fc32d12
--- /dev/null
+++ b/tests/files/observations.ttl
@@ -0,0 +1,93 @@
+@prefix rdf: .
+@prefix dc: .
+@prefix dcterms: .
+@prefix qb: .
+@prefix rdfs: .
+@prefix owl: .
+@prefix skos: .
+@prefix xsd: .
+@prefix sdmx: .
+@prefix sdmx-concept: .
+@prefix sdmx-dimension: .
+@prefix sdmx-attribute: .
+@prefix sdmx-measure: .
+@prefix sdmx-metadata: .
+@prefix sdmx-code: .
+@prefix sdmx-subject: .
+
+ a qb:DataSet ;
+dcterms:issued "2022-04-01T08:00:00.000"^^xsd:dateTime ;
+dcterms:publisher ;
+dcterms:title "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr ;
+qb:structure ;
+rdfs:label "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr ;
+sdmx-attribute:title "GDP and main components (current prices)"@en, "PIB et principales composantes (prix courants)"@fr .
+
+ a qb:Observation;
+ ;
+ "W2" ;
+ "S1" ;
+ "S1" ;
+ "B" ;
+ "B1G" ;
+ "_Z" ;
+ "A" ;
+ "_Z" ;
+ "XDC" ;
+ "V" ;
+ "N" ;
+qb:dataSet ;
+sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+sdmx-attribute:decimals sdmx-code:decimals-1 ;
+sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+sdmx-dimension:freq sdmx-code:freq-A ;
+sdmx-dimension:refArea "BE" ;
+sdmx-dimension:timePeriod "2011" ;
+sdmx-measure:obsValue "1016.9"^^xsd:float .
+
+ a qb:Observation;
+ ;
+ "W2" ;
+ "S1" ;
+ "S1" ;
+ "B" ;
+ "B1G" ;
+ "_Z" ;
+ "A" ;
+ "_Z" ;
+ "XDC" ;
+ "V" ;
+ "N" ;
+qb:dataSet ;
+sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+sdmx-attribute:decimals sdmx-code:decimals-1 ;
+sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+sdmx-dimension:freq sdmx-code:freq-A ;
+sdmx-dimension:refArea "BE" ;
+sdmx-dimension:timePeriod "2012" ;
+sdmx-measure:obsValue "3016.9"^^xsd:float .
+
+ a qb:Observation;
+ ;
+ "W2" ;
+ "S1" ;
+ "S1" ;
+ "B" ;
+ "B1G" ;
+ "_Z" ;
+ "A" ;
+ "_Z" ;
+ "XDC" ;
+ "V" ;
+ "N" ;
+qb:dataSet ;
+sdmx-attribute:confStatus sdmx-code:confStatus-F ;
+sdmx-attribute:decimals sdmx-code:decimals-1 ;
+sdmx-attribute:obsStatus sdmx-code:obsStatus-A ;
+sdmx-attribute:unitMult sdmx-code:unitMult-6 ;
+sdmx-dimension:freq sdmx-code:freq-A ;
+sdmx-dimension:refArea "BE" ;
+sdmx-dimension:timePeriod "2013" ;
+sdmx-measure:obsValue "9016.9"^^xsd:float .
diff --git a/examples/structures-accounts.ttl b/tests/files/structures-accounts.ttl
similarity index 100%
rename from examples/structures-accounts.ttl
rename to tests/files/structures-accounts.ttl
diff --git a/examples/structures-tourism.ttl b/tests/files/structures-tourism.ttl
similarity index 100%
rename from examples/structures-tourism.ttl
rename to tests/files/structures-tourism.ttl
diff --git a/tests/sdmxattributes/__init__.py b/tests/sdmxattributes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/sdmxattributes/common/config.json b/tests/sdmxattributes/common/config.json
new file mode 100644
index 0000000..a06283f
--- /dev/null
+++ b/tests/sdmxattributes/common/config.json
@@ -0,0 +1,10 @@
+{
+ "broker": "http://127.0.0.1:1026",
+ "logger": {
+ "path": "./logs/access.log",
+ "level": "debug",
+ "rotation": "20 days",
+ "retention": "1 months",
+ "format": "{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} request id: {extra[request_id]} - {name}:{function} - {message}"
+ }
+}
diff --git a/tests/sdmxattributes/test_code.py b/tests/sdmxattributes/test_code.py
new file mode 100644
index 0000000..f79db6d
--- /dev/null
+++ b/tests/sdmxattributes/test_code.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.sdmxattributes.code import Code
+from sdmx2jsonld.exceptions.exceptions import ClassCode
+
+
+class TestConfStatus(TestCase):
+ def test_code_value_with_prefix(self):
+ value = "sdmx-code:decimals-1"
+ expected = 1
+
+ code = Code(typecode="decimals")
+ obtained = code.fix_value(value=value)
+ assert obtained == expected, (
+ f"\ncode was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_code_negative_value_with_prefix(self):
+ value = "sdmx-code:decimals--1"
+ expected = "sdmx-code:decimals--1 -> decimals out of range, got: -1 range(0, 15)"
+
+ code = Code(typecode="decimals")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_code_value_bigger_than_maximum_with_prefix(self):
+ value = "sdmx-code:decimals-67"
+ expected = "sdmx-code:decimals-67 -> decimals out of range, got: 67 range(0, 15)"
+
+ code = Code(typecode="decimals")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_code_value_without_prefix(self):
+ value = "unitMult-1"
+ expected = 1
+
+ code = Code(typecode="unitMult")
+ obtained = code.fix_value(value=value)
+ assert obtained == expected, (
+ f"\ncode was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_code_negative_value_without_prefix(self):
+ value = "unitMult--1"
+ expected = "unitMult--1 -> unitMult out of range, got: -1 range(0, 13)"
+
+ code = Code(typecode="unitMult")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_code_value_bigger_than_maximum_without_prefix(self):
+ value = "unitMult-67"
+ expected = "unitMult-67 -> unitMult out of range, got: 67 range(0, 13)"
+
+ code = Code(typecode="unitMult")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_code_integer_value(self):
+ value = 2
+ expected = 2
+
+ code = Code(typecode="unitMult")
+ obtained = code.fix_value(value=value)
+ assert obtained == expected, (
+ f"\ncode was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_code_integer_value_out_of_range(self):
+ value = 25
+ expected = "25 -> unitMult out of range, got: 25 range(0, 13)"
+
+ code = Code(typecode="unitMult")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_code_string_value(self):
+ value = "2"
+ expected = 2
+
+ code = Code(typecode="unitMult")
+ obtained = code.fix_value(value=value)
+ assert obtained == expected, (
+ f"\ncode was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_code_string_value_out_of_range(self):
+ value = "25"
+ expected = "25 -> unitMult out of range, got: 25 range(0, 13)"
+
+ code = Code(typecode="unitMult")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_any_other_code(self):
+ value = "sadf"
+ expected = "sadf -> Data is not a valid value"
+
+ code = Code(typecode="unitMult")
+
+ with self.assertRaises(ClassCode) as error:
+ _ = code.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
diff --git a/tests/sdmxattributes/test_confirmationStatus.py b/tests/sdmxattributes/test_confirmationStatus.py
new file mode 100644
index 0000000..b5406b4
--- /dev/null
+++ b/tests/sdmxattributes/test_confirmationStatus.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.sdmxattributes.confirmationStatus import ConfStatus
+from sdmx2jsonld.exceptions.exceptions import ClassConfStatusError
+
+
+class TestConfStatus(TestCase):
+ def setUp(self):
+ self.conversion = ConfStatus()
+
+ def test_fix_value_data_in_predefined_values(self):
+ value = "F"
+ expected = "F"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nconfStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ value = "f"
+ expected = "F"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nconfStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_fix_value_data_in_predefined_values_with_prefix(self):
+ value = "confStatus-A"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nconfStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ value = "confstatus-a"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nconfStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_fix_value_data_with_valid_prefix_but_not_expected_value(self):
+ value = "confStatus-EEEEE"
+ expected = (
+ "confStatus-EEEEE -> ConfStatus value is not included in the list of available values,\n"
+ + " got:confStatus-EEEEE\n"
+ + " expected:['confStatus-F', 'confStatus-N', 'confStatus-C', 'confStatus-D', 'confStatus-S', "
+ "'confStatus-A', 'confStatus-O', 'confStatus-T', 'confStatus-G', 'confStatus-M', 'confStatus-E', "
+ "'confStatus-P']"
+ )
+
+ with self.assertRaises(ClassConfStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_fix_value_unexpected_value_without_prefix(self):
+ value = "EEEE"
+ expected = "EEEE -> ConfStatus value is not the expected"
+ with self.assertRaises(ClassConfStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_fix_value_unexpected_prefix(self):
+ value = "lkdjlks-A"
+ expected = "lkdjlks-A -> ConfStatus value is not the expected"
+ with self.assertRaises(ClassConfStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
diff --git a/tests/sdmxattributes/test_examples.py b/tests/sdmxattributes/test_examples.py
new file mode 100644
index 0000000..dcf0c29
--- /dev/null
+++ b/tests/sdmxattributes/test_examples.py
@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.transform.parser import Parser
+from io import StringIO
+from os import listdir, unlink
+from os.path import join, isfile, dirname
+from pathlib import Path
+
+
+class TestCommonClass(TestCase):
+ def setUp(self) -> None:
+ examples_folder = dirname(dirname(__file__))
+ output_folder = dirname(examples_folder)
+
+ examples_folder = join(examples_folder, "files")
+ self.output_folder = join(output_folder, "output")
+
+ tests_files = [
+ "observations.ttl",
+ "structures-accounts.ttl",
+ "structures-tourism.ttl",
+ ]
+ self.tests_files = [join(examples_folder, x) for x in tests_files]
+
+ self.parser = Parser()
+
+ def clean_output_dir(self) -> None:
+ for a in listdir(self.output_folder):
+ file_path = join(self.output_folder, a)
+ try:
+ if isfile(file_path):
+ unlink(file_path)
+ except Exception as e:
+ print("----------------------------")
+ print(e)
+ print("----------------------------")
+
+ def test_files_from_StringIO_web_interface_loop(self):
+ print("Testing test_files_from_StringIO_web_interface_loop")
+ for a in self.tests_files:
+ print(f"Parsing: {a}")
+ self.clean_output_dir()
+
+ # Read the RDF Turtle file
+ with open(a, "r") as rf:
+ rdf_data = rf.read()
+
+ # Parsing the RDF
+ try:
+ _ = self.parser.parsing(content=StringIO(rdf_data), out=False)
+ except Exception as e:
+ assert False, f"\nThe parser was not completed," f"\n file: {a}" f"\n exception:\n {e.message}"
+
+ print("Parsing completed...\n")
+
+ print("Test finished...\n")
+
+ def test_files_from_TextIOWrapper_cli_with_generating_files(self):
+ print("Testing test_files_from_TextIOWrapper_cli_with_generating_files")
+ for a in self.tests_files:
+ print(f"Parsing: {a}")
+ self.clean_output_dir()
+
+ # Read the RDF Turtle file
+ with open(a, "r") as rf:
+ rdf_data = rf.read()
+
+ # Parsing the RDF
+ try:
+ _ = self.parser.parsing(content=rdf_data, out=True)
+ except Exception as e:
+ assert False, f"\nThe parser was not completed," f"\n file: {a}" f"\n exception:\n {e.message}"
+
+ print("Parsing completed...\n")
+
+ print("Test finished...\n")
+
+ def test_file_from_TextIOWrapper_cli_only_printing_result(self):
+ print("Testing test_files_from_TextIOWrapper_cli_with_generating_files")
+ for a in self.tests_files:
+ print(f"Parsing: {a}")
+ self.clean_output_dir()
+
+ # Read the RDF Turtle file
+ with open(a, "r") as rf:
+ rdf_data = rf.read()
+
+ # Parsing the RDF
+ try:
+ _ = self.parser.parsing(content=rdf_data, out=False)
+ except Exception as e:
+ assert False, f"\nThe parser was not completed," f"\n file: {a}" f"\n exception:\n {e.message}"
+
+ print("Parsing completed...\n")
+
+ print("Test finished...\n")
diff --git a/tests/sdmxattributes/test_observationStatus.py b/tests/sdmxattributes/test_observationStatus.py
new file mode 100644
index 0000000..48195e9
--- /dev/null
+++ b/tests/sdmxattributes/test_observationStatus.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+##
+# Copyright 2022 FIWARE Foundation, e.V.
+#
+# This file is part of IoTAgent-SDMX (RDF Turtle)
+#
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+from unittest import TestCase
+from sdmx2jsonld.sdmxattributes.observationStatus import ObsStatus
+from sdmx2jsonld.exceptions.exceptions import ClassObsStatusError
+
+
+class TestObsStatus(TestCase):
+ def setUp(self):
+ self.conversion = ObsStatus()
+
+ def test_fix_value_data_in_predefined_values(self):
+ value = "A"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nobsStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ value = "a"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nobsStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_fix_value_data_in_predefined_values_with_prefix(self):
+ value = "obsStatus-A"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nobsStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ value = "obsstatus-a"
+ expected = "A"
+ obtained = self.conversion.fix_value(value=value)
+ assert obtained == expected, (
+ f"\nobsStatus was not the expected," f"\n got : {obtained}" f"\n expected: {expected}"
+ )
+
+ def test_fix_value_data_with_valid_prefix_but_not_expected_value(self):
+ value = "obsStatus-EEEEE"
+ expected = (
+ "obsStatus-EEEEE -> ObsStatus value is not included in the list of available values,\n"
+ + " got:obsStatus-EEEEE\n"
+ + " expected:['obsStatus-A', 'obsStatus-B', 'obsStatus-D', 'obsStatus-E', 'obsStatus-F', "
+ "'obsStatus-G', 'obsStatus-I', 'obsStatus-K', 'obsStatus-W', 'obsStatus-O', 'obsStatus-M', "
+ "'obsStatus-P', 'obsStatus-S', 'obsStatus-L', 'obsStatus-H', 'obsStatus-Q', 'obsStatus-J', "
+ "'obsStatus-N', 'obsStatus-U', 'obsStatus-V']"
+ )
+
+ with self.assertRaises(ClassObsStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_fix_value_unexpected_value_without_prefix(self):
+ value = "EEEE"
+ expected = "EEEE -> ObsStatus value is not the expected"
+ with self.assertRaises(ClassObsStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
+
+ def test_fix_value_unexpected_prefix(self):
+ value = "lkdjlks-A"
+ expected = "lkdjlks-A -> ObsStatus value is not the expected"
+ with self.assertRaises(ClassObsStatusError) as error:
+ _ = self.conversion.fix_value(value=value)
+
+ self.assertEqual(str(error.exception), expected)
diff --git a/tox.ini b/tox.ini
old mode 100644
new mode 100755
index 457561e..ba8e7fa
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,50 @@
[tox]
-envlist = py37, py38, py39, py310, py311
+requires =
+ tox>=4
+env_list = lint, type, py{310, 311}, coverage
-[testenv]
+[testenv:coverage]
+description = run coverage tests
+deps =
+ pytest>=7
+ pytest-sugar
+ nose
+ coverage
commands =
- pytest tests/
\ No newline at end of file
+ coverage erase
+ coverage run --source=./api,./cli,./common,./ngsild,./sdmx2jsonld --omit=./coverage_html_report,./dist,./docker,./docs,./examples,./images,./logs,./tests -m pytest
+ coverage report -m
+
+[testenv:py310]
+description = run unit tests in python3.10
+deps =
+ pytest>=7
+ pytest-sugar
+commands =
+ pytest {posargs:tests}
+
+[testenv:py311]
+description = run unit tests in python3.11
+deps =
+ pytest>=7
+ pytest-sugar
+commands =
+ pytest {posargs:tests}
+
+[testenv:lint]
+description = run linters
+skip_install = true
+deps =
+ black==22.12
+commands = black {posargs:.}
+
+[testenv:type]
+description = run type checks
+deps =
+ mypy>=0.991
+ types-docopt==0.6.11.3
+ types-python-dateutil==2.8.19.14
+ types-pytz==2023.3.0.0
+ types-requests==2.31.0.2
+commands =
+ mypy {posargs:.} --no-warn-unused-ignores