Coverage for portality/lib/modeldoc.py: 0%
86 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-19 16:52 +0100
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-19 16:52 +0100
1from portality.lib import dates, plugin
2from datetime import datetime
3import json
5DO_TYPE_TO_JSON_TYPE = {
6 "str": "string",
7 "utcdatetime": "timestamp",
8 "integer": 0,
9 "bool": True,
10 "float": 0.0,
11 "isolang": "string",
12 "url": "string",
13 "isolang_2letter": "string",
14 "bigenddate" : "datestamp"
15}
17DO_TYPE_TO_DATATYPE = {
18 "str": "str",
19 "utcdatetime": "str",
20 "integer": "int",
21 "bool": "bool",
22 "float": "float",
23 "isolang": "str",
24 "url": "str",
25 "isolang_2letter": "str",
26 "bigenddate" : "str"
27}
29DO_TYPE_TO_FORMAT = {
30 "str": "",
31 "utcdatetime": "UTC ISO formatted date: YYYY-MM-DDTHH:MM:SSZ",
32 "integer": "",
33 "bool": "",
34 "float": "",
35 "isolang": "3 letter ISO language code",
36 "url": "URL",
37 "isolang_2letter": "2 letter ISO language code",
38 "bigenddate" : "Date, year first: YYYY-MM-DD"
39}
41def format(klazz, example, fields):
42 title = "# " + klazz.__name__
44 intro = "The JSON structure of the model is as follows:"
46 struct = "```json\n" + json.dumps(example, indent=4, sort_keys=True) + "\n```"
48 table_intro = "Each of the fields is defined as laid out in the table below. All fields are optional unless otherwise specified:"
50 table = "| Field | Description | Datatype | Format | Allowed Values |\n"
51 table += "| ----- | ----------- | -------- | ------ | -------------- |\n"
53 keys = list(fields.keys())
54 keys.sort()
56 for k in keys:
57 desc, datatype, format, values = fields.get(k)
58 table += "| {field} | {desc} | {datatype} | {format} | {values} |\n".format(field=k, desc=desc, datatype=datatype, format=format, values=values)
60 return title + "\n\n" + intro + "\n\n" + struct + "\n\n" + table_intro + "\n\n" + table
62def document(klazz, field_descriptions):
63 inst = klazz()
64 base_struct = inst.get_struct()
66 fields = {}
68 def do_document(path, struct, fields):
69 example = {}
71 # first do all the fields at this level
72 for simple_field, instructions in struct.get('fields', {}).items():
73 example[simple_field] = type_map(instructions.get("coerce"))
74 fields[path + simple_field] = (field_descriptions.get(path + simple_field, ""), datatype(instructions.get("coerce")), form(instructions.get("coerce")), values_or_range(instructions.get("allowed_values"), instructions.get("allowed_range")))
76 # now do all the objects at this level
77 for obj in struct.get('objects', []):
78 newpath = obj + "." if not path else path + obj + "."
79 instructions = struct.get('structs', {}).get(obj, {})
80 example[obj] = do_document(newpath, instructions, fields)
82 # finally do all the lists at this level
83 for l, instructions in struct.get('lists', {}).items():
84 if instructions['contains'] == 'field':
85 example[l] = [type_map(instructions.get("coerce"))]
86 fields[path + l] = (field_descriptions.get(path + l, ""), datatype(instructions.get("coerce")), form(instructions.get("coerce")), values_or_range(instructions.get("allowed_values"), instructions.get("allowed_range")))
88 elif instructions['contains'] == 'object':
89 newpath = l + "." if not path else path + l + "."
90 inst = struct.get('structs', {}).get(l, {})
91 example[l] = [do_document(newpath, inst, fields)]
93 return example
95 example = do_document("", base_struct, fields)
97 return example, fields
99def type_map(t):
100 type = DO_TYPE_TO_JSON_TYPE.get(t, "string")
101 if type == "timestamp":
102 return dates.now()
103 elif type == "datestamp":
104 return dates.format(datetime.utcnow(), "%Y-%m-%d")
105 return type
107def datatype(t):
108 return DO_TYPE_TO_DATATYPE.get(t, "str")
110def form(t):
111 return DO_TYPE_TO_FORMAT.get(t, "")
113def values_or_range(vals, range):
114 if vals is not None:
115 return ", ".join(vals)
116 if range is not None:
117 lower, upper = range
118 if lower is not None and upper is not None:
119 return lower + " to " + upper
120 elif lower is not None and upper is None:
121 return "less than " + lower
122 elif lower is None and upper is not None:
123 return "greater than " + upper
124 return ""
126if __name__ == "__main__":
127 import argparse
128 parser = argparse.ArgumentParser()
129 parser.add_argument("-k", "--klazz", help="class to document")
130 parser.add_argument("-o", "--out", help="output file")
131 parser.add_argument("-f", "--fields", help="field descriptions table")
132 args = parser.parse_args()
134 descriptions = {}
135 if args.fields:
136 with open(args.fields) as f:
137 fds = f.read()
138 lines = fds.split("\n")
139 for line in lines:
140 sep = line.find(":")
141 descriptions[line[:sep]] = line[sep + 1:].strip()
143 k = plugin.load_class_raw(args.klazz)
144 example, fields = document(k, descriptions)
145 doc = format(k, example, fields)
147 with open(args.out, "w") as f:
148 f.write(doc)