Coverage for portality / lib / modeldoc.py: 0%

86 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 09:41 +0100

1from portality.lib import dates, plugin 

2import json 

3 

4from portality.lib.dates import FMT_DATE_STD 

5 

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} 

17 

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} 

29 

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} 

41 

42def format(klazz, example, fields): 

43 title = "# " + klazz.__name__ 

44 

45 intro = "The JSON structure of the model is as follows:" 

46 

47 struct = "```json\n" + json.dumps(example, indent=4, sort_keys=True) + "\n```" 

48 

49 table_intro = "Each of the fields is defined as laid out in the table below. All fields are optional unless otherwise specified:" 

50 

51 table = "| Field | Description | Datatype | Format | Allowed Values |\n" 

52 table += "| ----- | ----------- | -------- | ------ | -------------- |\n" 

53 

54 keys = list(fields.keys()) 

55 keys.sort() 

56 

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) 

60 

61 return title + "\n\n" + intro + "\n\n" + struct + "\n\n" + table_intro + "\n\n" + table 

62 

63def document(klazz, field_descriptions): 

64 inst = klazz() 

65 base_struct = inst.get_struct() 

66 

67 fields = {} 

68 

69 def do_document(path, struct, fields): 

70 example = {} 

71 

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"))) 

76 

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) 

82 

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"))) 

88 

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)] 

93 

94 return example 

95 

96 example = do_document("", base_struct, fields) 

97 

98 return example, fields 

99 

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 

107 

108def datatype(t): 

109 return DO_TYPE_TO_DATATYPE.get(t, "str") 

110 

111def form(t): 

112 return DO_TYPE_TO_FORMAT.get(t, "") 

113 

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 "" 

126 

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() 

134 

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() 

143 

144 k = plugin.load_class_raw(args.klazz) 

145 example, fields = document(k, descriptions) 

146 doc = format(k, example, fields) 

147 

148 with open(args.out, "w") as f: 

149 f.write(doc)