Coverage for portality / lib / modeldoc.py: 0%
86 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
1from portality.lib import dates, plugin
2import json
4from portality.lib.dates import FMT_DATE_STD
6DO_TYPE_TO_JSON_TYPE = {
7 "str": "string",
8 "utcdatetime": "timestamp",
9 "integer": 0,
10 "bool": True,
11 "float": 0.0,
12 "isolang": "string",
13 "url": "string",
14 "isolang_2letter": "string",
15 "bigenddate" : "datestamp"
16}
18DO_TYPE_TO_DATATYPE = {
19 "str": "str",
20 "utcdatetime": "str",
21 "integer": "int",
22 "bool": "bool",
23 "float": "float",
24 "isolang": "str",
25 "url": "str",
26 "isolang_2letter": "str",
27 "bigenddate" : "str"
28}
30DO_TYPE_TO_FORMAT = {
31 "str": "",
32 "utcdatetime": "UTC ISO formatted date: YYYY-MM-DDTHH:MM:SSZ",
33 "integer": "",
34 "bool": "",
35 "float": "",
36 "isolang": "3 letter ISO language code",
37 "url": "URL",
38 "isolang_2letter": "2 letter ISO language code",
39 "bigenddate" : "Date, year first: YYYY-MM-DD"
40}
42def format(klazz, example, fields):
43 title = "# " + klazz.__name__
45 intro = "The JSON structure of the model is as follows:"
47 struct = "```json\n" + json.dumps(example, indent=4, sort_keys=True) + "\n```"
49 table_intro = "Each of the fields is defined as laid out in the table below. All fields are optional unless otherwise specified:"
51 table = "| Field | Description | Datatype | Format | Allowed Values |\n"
52 table += "| ----- | ----------- | -------- | ------ | -------------- |\n"
54 keys = list(fields.keys())
55 keys.sort()
57 for k in keys:
58 desc, datatype, format, values = fields.get(k)
59 table += "| {field} | {desc} | {datatype} | {format} | {values} |\n".format(field=k, desc=desc, datatype=datatype, format=format, values=values)
61 return title + "\n\n" + intro + "\n\n" + struct + "\n\n" + table_intro + "\n\n" + table
63def document(klazz, field_descriptions):
64 inst = klazz()
65 base_struct = inst.get_struct()
67 fields = {}
69 def do_document(path, struct, fields):
70 example = {}
72 # first do all the fields at this level
73 for simple_field, instructions in struct.get('fields', {}).items():
74 example[simple_field] = type_map(instructions.get("coerce"))
75 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")))
77 # now do all the objects at this level
78 for obj in struct.get('objects', []):
79 newpath = obj + "." if not path else path + obj + "."
80 instructions = struct.get('structs', {}).get(obj, {})
81 example[obj] = do_document(newpath, instructions, fields)
83 # finally do all the lists at this level
84 for l, instructions in struct.get('lists', {}).items():
85 if instructions['contains'] == 'field':
86 example[l] = [type_map(instructions.get("coerce"))]
87 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")))
89 elif instructions['contains'] == 'object':
90 newpath = l + "." if not path else path + l + "."
91 inst = struct.get('structs', {}).get(l, {})
92 example[l] = [do_document(newpath, inst, fields)]
94 return example
96 example = do_document("", base_struct, fields)
98 return example, fields
100def type_map(t):
101 type = DO_TYPE_TO_JSON_TYPE.get(t, "string")
102 if type == "timestamp":
103 return dates.now_str()
104 elif type == "datestamp":
105 return dates.now_str(FMT_DATE_STD)
106 return type
108def datatype(t):
109 return DO_TYPE_TO_DATATYPE.get(t, "str")
111def form(t):
112 return DO_TYPE_TO_FORMAT.get(t, "")
114def values_or_range(vals, range):
115 if vals is not None:
116 return ", ".join(vals)
117 if range is not None:
118 lower, upper = range
119 if lower is not None and upper is not None:
120 return lower + " to " + upper
121 elif lower is not None and upper is None:
122 return "less than " + lower
123 elif lower is None and upper is not None:
124 return "greater than " + upper
125 return ""
127if __name__ == "__main__":
128 import argparse
129 parser = argparse.ArgumentParser()
130 parser.add_argument("-k", "--klazz", help="class to document")
131 parser.add_argument("-o", "--out", help="output file")
132 parser.add_argument("-f", "--fields", help="field descriptions table")
133 args = parser.parse_args()
135 descriptions = {}
136 if args.fields:
137 with open(args.fields) as f:
138 fds = f.read()
139 lines = fds.split("\n")
140 for line in lines:
141 sep = line.find(":")
142 descriptions[line[:sep]] = line[sep + 1:].strip()
144 k = plugin.load_class_raw(args.klazz)
145 example, fields = document(k, descriptions)
146 doc = format(k, example, fields)
148 with open(args.out, "w") as f:
149 f.write(doc)