Ya construye XSD aunque con errores

This commit is contained in:
perro tuerto 2023-01-27 19:58:35 -08:00
parent adc543e608
commit 5e21d1f3b3
2 changed files with 124 additions and 37 deletions

View File

@ -73,10 +73,10 @@ To better understad what YASD does, see the inputs and outputs:
...
children_order: all|choice|sequence
children:
- name: element_name
maxOccurs: INTEGER
- ref: element_name
maxOccurs: INTEGER|unbounded
minOccurs: INTEGER
- group: group_name
- group_ref: group_name
...
### Attribute (ATTR) structure
@ -97,7 +97,7 @@ To better understad what YASD does, see the inputs and outputs:
attribute_group: true|false
children_order: all|choice|sequence
children:
- name: element_name
- ref: element_or_attribute_name
maxOccurs: INTEGER|unbounded
minOccurs: INTEGER
@ -137,7 +137,7 @@ Mandatory.
### `name`
Indicates element or attribute name.
Indicates element, attribute or group name.
Mandatory.
@ -152,6 +152,12 @@ Naming rules:
periods
- Element names cannot contain spaces
### `ref`
References element or attribute by name.
Mandatory.
### `description`
Indicates element or attribute description in human readable form.
@ -174,13 +180,13 @@ Allowed types:
Chart:
| type | elements | text | attributes |
|------------|:--------:|:----:|:----------:|
|simple | ✗ | ✓ | ✗ |
|empty | ✗ | ✗ | ✓ |
|no_text | ✓ | ✗ | ✓ |
|no_elements | ✗ | ✓ | ✓ |
|mixed | ✓ | ✓ | ✓ |
| type | elements | text | attributes |
|-------------|:--------:|:----:|:----------:|
| simple | ✗ | ✓ | ✗ |
| empty | ✗ | ✗ | ✓ |
| no_text | ✓ | ✗ | ✓ |
| no_elements | ✗ | ✓ | ✓ |
| mixed | ✓ | ✓ | ✓ |
> **Note 1**: read "elements" and "text" with "direct children…" as prefix.
@ -353,7 +359,7 @@ Valid value is non negative integer.
### `group`
Indicates group name.
References group name.
Optional.

129
yasd.py
View File

@ -2,11 +2,12 @@
# (c) 2023 Perro Tuerto <hi@perrotuerto.blog>.
# Founded by Mexican Academy of Language <https://academia.org.mx>.
# Licensed under GPLv3 <https://www.gnu.org/licenses/gpl-3.0.en.html>.
# Requirements: python > 3.10, pyyaml, lxml, bs4, rich
# Requirements: python > 3.10, pyyaml, lxml, bs4, xmlschema, rich
import sys
import yaml
import argparse
import xmlschema
import urllib.request
from pathlib import Path
from bs4 import BeautifulSoup
@ -27,6 +28,7 @@ class YASD:
quiet=False,
log=False,
stdout=False,
validate=False,
):
"""
Performs YASD actions directly.
@ -44,11 +46,13 @@ class YASD:
:type log: True or False
:param stdout: if conversion goes to stdout or not; 'False' by default
:type stdout: True or False
:param validate: if XSD is validated; 'False' by default
:type validate: True or False
:return: Output data; str on 'document'; bs4.element.Tag on 'convert'
or 'sample'; YAML dict on 'check'
:rtype: str or bs4.element.Tag or dict
"""
yasd = YASD(indata, outfile, quiet, log, stdout)
yasd = YASD(indata, outfile, quiet, log, stdout, validate)
yasd.msgr.run(f"action_{action}")
match action:
case "document":
@ -61,7 +65,13 @@ class YASD:
return yasd.yaml
def __init__(
self, indata=None, outfile=None, quiet=False, log=False, stdout=False
self,
indata=None,
outfile=None,
quiet=False,
log=False,
stdout=False,
validate=False,
):
"""
Inits YASD object.
@ -74,11 +84,16 @@ class YASD:
:type quiet: True or False
:param log: If messages are write in a file or not; 'False' by default
:type log: True or False
:param stdout: if conversion goes to stdout or not; 'False' by default
:type stdout: True or False
:param validate: if XSD is validated; 'False' by default
:type validate: True or False
"""
self.msgr = YASDMessenger(quiet=quiet, log=log)
self.yaml = YASDCheck(indata, self.msgr).yaml
self.formatter = XMLFormatter(indent=2)
self.stdout = stdout
self.validate = validate
if outfile is None:
self.outfile = None
else:
@ -92,7 +107,11 @@ class YASD:
:rtype: bs4.element.Tag
"""
self.xsd = YASDXSD(self.yaml, self.msgr).xsd
return self.__output(self.xsd)
out = self.__output(self.xsd)
if self.validate:
return YASDCheck.xsd(out, self.msgr)
else:
return out
def sample(self):
"""
@ -118,13 +137,12 @@ class YASD:
"""
Prints in the terminal or writes into a file.
:return: Output data; str if outdata comes from YASDRST; otherwise
bs4.element.Tag
:rtype: str or bs4.element.Tag
:return: Output data
:rtype: str
"""
if type(outdata) is not str:
outdata = outdata.prettify(formatter=self.formatter)
if self.stdout:
if type(outdata) is not str:
outdata = outdata.prettify(formatter=self.formatter)
if self.outfile is None:
print(outdata)
else:
@ -184,7 +202,7 @@ class YASDXSD:
def __build_attributes(self):
"""
Builds attributes nodes for XSD.
Builds attribute nodes for XSD.
Attributes are always simple types.
"""
@ -192,8 +210,18 @@ class YASDXSD:
self.__build_simple(self.__sanitize(el), tag="attribute")
def __build_groups(self):
# TODO build groups
...
"""
Builds group nodes for XSD.
"""
for el in self.yaml["groups"]:
if "attribute_group" in el.keys():
# TODO build attributeGroup
...
else:
element = self.xsd.new_tag("group", nsprefix="xs")
indicator = self.__build_indicator(el)
element.append(indicator)
self.xsd.schema.append(element)
def __build_simple(self, el, tag="element"):
"""
@ -224,9 +252,7 @@ class YASDXSD:
complex_type = self.__build_complex_type(el)
self.__add_references(complex_type, el, is_attr=True)
if "children" in el.keys():
other_tag = el["children_order"]
indicator = self.xsd.new_tag(other_tag, nsprefix="xs")
self.__add_references(indicator, el)
indicator = self.__build_indicator(el)
complex_type.append(indicator)
element.append(complex_type)
self.xsd.schema.append(element)
@ -293,6 +319,17 @@ class YASDXSD:
container.append(restriction)
root.append(container)
def __build_indicator(self, el):
"""
Builds indicator node for XSD.
:param dict el: YASD element
"""
other_tag = el["children_order"]
indicator = self.xsd.new_tag(other_tag, nsprefix="xs")
self.__add_references(indicator, el)
return indicator
def __get_base(self, restrictions):
"""
Gets restriction data type.
@ -325,7 +362,7 @@ class YASDXSD:
if key in el.keys() and "group" in el[key][0].keys():
tag, name = "group", "group"
else:
name = "name"
name = "ref"
return key, tag, name
def __add_references(self, root, el, is_attr=False):
@ -342,6 +379,10 @@ class YASDXSD:
for element in el[key]:
node = self.xsd.new_tag(tag, nsprefix="xs")
node["ref"] = element[name]
if "maxOccurs" in element.keys():
node["maxOccurs"] = element["maxOccurs"]
if "minOccurs" in element.keys():
node["minOccurs"] = element["minOccurs"]
root.append(node)
del el[key]
@ -427,10 +468,12 @@ class YASDCheck:
def file(filepath=None, msgr=None):
"""
Verififes if file exists.
Verifies if file exists.
:param filepath: File path
:type filepath: None or Path
:param msgr: Messenger object
:type msgr: None or YASDMessenger
"""
msgr = YASDCheck.messenger(msgr)
if type(filepath).__module__ != "pathlib":
@ -439,6 +482,40 @@ class YASDCheck:
msgr.run("invalid_input", level="error", file=filepath)
return filepath.resolve()
def url(key="readme", msgr=None):
"""
Verifies if remote file exists
:param str key: YASDMessenger string key
:param msgr: Messenger object
:type msgr: None or YASDMessenger
"""
# TODO if YASD becomes pip package, the fetched files should be local
# Remove urllib import if that is the case
msgr = YASDCheck.messenger(msgr)
try:
url = YASDMessenger.keys()[key]
msgr.run("fetching", url=url)
return urllib.request.urlopen(url).read().decode("utf-8")
except Exception:
msgr.run("no_url", level="error", url=url)
def xsd(xsd, msgr=None):
"""
Validates XSD.
:param str xsd: XSD as XML string
:param msgr: Messenger object
:type msgr: None or YASDMessenger
"""
msgr = YASDCheck.messenger(msgr)
msgr.run("validating")
try:
xmlschema.XMLSchema(xsd)
except xmlschema.validators.exceptions.XMLSchemaParseError as error:
error = str(error).replace("\n", "\n ")
msgr.run("no_valid", error=error, level="error")
def __init__(self, indata=None, messenger=None):
"""
Inits YASD validator.
@ -502,20 +579,26 @@ class YASDMessenger:
Academy of Language <https://academia.org.mx>. Licensed under GPLv3
<https://www.gnu.org/licenses/gpl-3.0.en.html>.
""",
"readme": "https://gitlab.com/perrotuerto_personal/codigo/yasd/-/raw/no-masters/README.md",
"w3": "https://www.w3.org/2001/XMLSchema.xsd",
"help_action": "action to perform",
"help_input": "input file in YAML format",
"help_output": "output file",
"help_quiet": "enable quiet mode",
"help_log": "write log",
"action_convert": "creating XSD schema",
"action_check": "checking YASD",
"action_check": "checking YASD structure",
"action_sample": "creating XML sample",
"action_document": "creating RST documentation",
"invalid_level": "invalid log level '@lvl'",
"invalid_input": "invalid file '@file'",
"invalid_yaml": "invalid YAML structure",
"no_yaml": "YAML dict needed",
"no_url": "failed to fetch '@url'",
"no_input": "input file needed",
"no_yaml": "YAML dict needed",
"no_valid": "XSD schema has the following error:\n @error",
"fetching": "fetching '@url'",
"validating": "validating XSD schema",
}
def __init__(self, quiet=False, log=False):
@ -582,10 +665,7 @@ class YASDCLI:
"""
Prints README as manual.
"""
# TODO if YASD becomes pip package, it should load local README
# Remove urllib import if that is the case
url = "https://gitlab.com/perrotuerto_personal/codigo/yasd/-/raw/no-masters/README.md"
raw = urllib.request.urlopen(url).read().decode("utf-8")
raw = YASDCheck.url()
raw = raw.replace("## Table of Contents\n\n[TOC]\n\n", "")
md = Markdown(raw)
console = Console()
@ -607,7 +687,8 @@ class YASDCLI:
args.output,
args.quiet,
args.log,
True,
stdout=True,
validate=True,
)
def __init_parser(self):