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

1from portality.lib import dates, plugin 

2from datetime import datetime 

3import json 

4 

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} 

16 

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} 

28 

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} 

40 

41def format(klazz, example, fields): 

42 title = "# " + klazz.__name__ 

43 

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

45 

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

47 

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

49 

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

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

52 

53 keys = list(fields.keys()) 

54 keys.sort() 

55 

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) 

59 

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

61 

62def document(klazz, field_descriptions): 

63 inst = klazz() 

64 base_struct = inst.get_struct() 

65 

66 fields = {} 

67 

68 def do_document(path, struct, fields): 

69 example = {} 

70 

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

75 

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) 

81 

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

87 

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

92 

93 return example 

94 

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

96 

97 return example, fields 

98 

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 

106 

107def datatype(t): 

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

109 

110def form(t): 

111 return DO_TYPE_TO_FORMAT.get(t, "") 

112 

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

125 

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

133 

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

142 

143 k = plugin.load_class_raw(args.klazz) 

144 example, fields = document(k, descriptions) 

145 doc = format(k, example, fields) 

146 

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

148 f.write(doc)