valar 1.0.21__py3-none-any.whl → 1.0.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of valar might be problematic. Click here for more details.

Files changed (63) hide show
  1. valar/__init__.py +0 -26
  2. valar/channels/__init__.py +1 -107
  3. valar/channels/consumer.py +48 -0
  4. valar/channels/executer.py +13 -0
  5. valar/channels/mapping.py +21 -0
  6. valar/channels/sender.py +60 -0
  7. valar/channels/views.py +6 -11
  8. valar/core/__init__.py +0 -0
  9. valar/core/counter.py +9 -0
  10. valar/core/dao/__init__.py +0 -0
  11. valar/core/dao/_mon_array2tree.py +18 -0
  12. valar/core/dao/dao_base.py +50 -0
  13. valar/core/dao/dao_mon.py +76 -0
  14. valar/core/dao/dao_orm.py +96 -0
  15. valar/core/dao/engine.py +12 -0
  16. valar/core/dao/engine_minio.py +90 -0
  17. valar/core/dao/engine_mon.py +34 -0
  18. valar/core/dao/engine_orm.py +25 -0
  19. valar/core/dao/model_mon.py +24 -0
  20. valar/core/dao/model_orm.py +192 -0
  21. valar/core/dao/query_mon.py +12 -0
  22. valar/core/dao/query_orm.py +43 -0
  23. valar/core/dao/utils_orm.py +85 -0
  24. valar/core/dao_abstract.py +63 -0
  25. valar/core/meta/__init__.py +0 -0
  26. valar/core/meta/defaults/__init__.py +0 -0
  27. valar/core/meta/defaults/field_keys_default.py +17 -0
  28. valar/core/meta/defaults/field_values_default.py +85 -0
  29. valar/core/meta/defaults/frame_defaults.py +136 -0
  30. valar/core/meta/defaults/view_defaults.py +7 -0
  31. valar/core/meta/field_orm.py +144 -0
  32. valar/core/meta/init_meta_frame.py +30 -0
  33. valar/core/meta/meta_orm.py +69 -0
  34. valar/core/middleware.py +20 -0
  35. valar/core/response.py +7 -0
  36. valar/core/singleton_meta.py +6 -0
  37. valar/core/valar_models.py +82 -0
  38. valar/data/migrations/0001_initial.py +141 -0
  39. valar/data/models.py +2 -121
  40. valar/data/urls.py +15 -21
  41. valar/data/views/__init__.py +0 -0
  42. valar/data/views/handler.py +41 -0
  43. valar/data/views/rest.py +86 -0
  44. {valar-1.0.21.dist-info → valar-1.0.23.dist-info}/METADATA +1 -1
  45. valar-1.0.23.dist-info/RECORD +50 -0
  46. {valar-1.0.21.dist-info → valar-1.0.23.dist-info}/WHEEL +1 -1
  47. valar/channels/utils.py +0 -43
  48. valar/data/file/__init__.py +0 -91
  49. valar/data/handlers.py +0 -28
  50. valar/data/mon/__init__.py +0 -123
  51. valar/data/mon/query_translator.py +0 -91
  52. valar/data/orm/__init__.py +0 -135
  53. valar/data/orm/detacher.py +0 -61
  54. valar/data/orm/meta.py +0 -99
  55. valar/data/orm/meta_frame.py +0 -100
  56. valar/data/orm/meta_loader.py +0 -200
  57. valar/data/orm/values.py +0 -102
  58. valar/data/query.py +0 -48
  59. valar/data/utils.py +0 -70
  60. valar/data/views.py +0 -173
  61. valar-1.0.21.dist-info/RECORD +0 -26
  62. {valar-1.0.21.dist-info → valar-1.0.23.dist-info}/licenses/LICENSE +0 -0
  63. {valar-1.0.21.dist-info → valar-1.0.23.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,34 @@
1
+ import pymongo
2
+ from django.conf import settings
3
+
4
+ from .model_mon import MonModel
5
+
6
+
7
+ class MonEngine:
8
+
9
+ def __init__(self):
10
+ uri = f'mongodb://localhost:27017/'
11
+ mongo = settings.MONGO
12
+ if mongo:
13
+ param = ['host', 'port', 'username', 'password']
14
+ host, port, username, password = [mongo.get(p) for p in param]
15
+ uri = f'mongodb://{username}:{password}@{host}:{port}/'
16
+
17
+ client = pymongo.MongoClient(uri, **{
18
+ 'maxPoolSize': 10,
19
+ 'minPoolSize': 0,
20
+ 'maxIdleTimeMS': 10000,
21
+ 'connectTimeoutMS': 10000,
22
+ 'socketTimeoutMS': 10000,
23
+ 'serverSelectionTimeoutMS': 10000,
24
+ })
25
+ database = client[settings.BASE_APP]
26
+ self.uri = uri
27
+ self.client = client
28
+ self.database = database
29
+
30
+ def get_mapping(self):
31
+ return {col['name']: self.database[col['name']] for col in self.database.list_collections()}
32
+
33
+ def get_model(self,entity)->MonModel:
34
+ return MonModel(self.database, entity)
@@ -0,0 +1,25 @@
1
+ from django.apps import apps
2
+
3
+ from .model_orm import OrmModel
4
+ from ..valar_models import VModel
5
+
6
+
7
+ class OrmEngine:
8
+
9
+ def __init__(self):
10
+ mapping = {}
11
+ for model in apps.get_models():
12
+ if issubclass(model, VModel):
13
+ path, name = model.__module__, model.__name__
14
+ prefix = 'src.valar.' if path.startswith('src') else 'valar.'
15
+ app = path.replace('.models', '').replace(prefix, '')
16
+ entity = '%s.%s' % (app, name)
17
+ mapping[entity] = model
18
+ self.mapping = mapping
19
+
20
+ def get_mapping(self)->dict:
21
+ return self.mapping
22
+
23
+ def get_model(self,entity)->OrmModel:
24
+ mod = self.mapping.get(entity)
25
+ return OrmModel(mod, entity)
@@ -0,0 +1,24 @@
1
+ from bson import ObjectId
2
+ from bson.errors import InvalidId
3
+ from pymongo.synchronous.collection import Collection
4
+
5
+
6
+ class MonModel:
7
+
8
+ def __init__(self, database, entity):
9
+ self.entity = entity
10
+ self.name = entity.replace('.', '_')
11
+ self.manager: Collection = database[self.name]
12
+
13
+ @staticmethod
14
+ def object_id(_id):
15
+ try:
16
+ return ObjectId(_id)
17
+ except(InvalidId, TypeError):
18
+ return None
19
+
20
+ def detach_item(self, item):
21
+ _id = item.get('id')
22
+ if _id:
23
+ del item['id']
24
+ return self.object_id(_id), item
@@ -0,0 +1,192 @@
1
+ from deepmerge import always_merger
2
+ from django.db.models import ManyToOneRel, ForeignKey, ManyToManyRel, ManyToManyField, OneToOneField, OneToOneRel
3
+ from django.db.models import QuerySet
4
+ from django.db.models import Manager
5
+ from django.db.models.fields.files import FieldFile
6
+ from django.forms import FileField
7
+
8
+ from .engine_minio import MinioEngine
9
+ from .utils_orm import OrmUtils
10
+ from ..meta.defaults.field_keys_default import meta_field_key_defaults
11
+ from ..meta.defaults.field_values_default import meta_field_value_defaults
12
+ from ..meta.field_orm import OrmField
13
+ from ..meta.init_meta_frame import init_meta_frame
14
+ from ..valar_models import VModel, VTree
15
+ from ...data.models import MetaFieldDomain
16
+
17
+
18
+ class OrmModel:
19
+
20
+ def __init__(self, mod, entity):
21
+ self.entity = entity
22
+ self.model = mod
23
+ meta = getattr(mod, '_meta')
24
+ self.name = meta.verbose_name
25
+ self.is_tree = issubclass(mod, VTree)
26
+ self.manager: Manager = mod.objects
27
+ fields = meta.get_fields()
28
+ mapping = {}
29
+ for f in fields:
30
+ field = OrmField(entity, f, self.is_tree)
31
+ mapping[field.prop] = field
32
+ self.mapping = mapping
33
+ self.minio = MinioEngine()
34
+ self.bucket_name = self.minio.get_bucket_name(entity)
35
+
36
+ @staticmethod
37
+ def object_id(_id):
38
+ return _id if isinstance(_id, int) and _id else None
39
+
40
+ def props(self, domain=None):
41
+ array = []
42
+ for prop in self.mapping:
43
+ field: OrmField = self.mapping[prop]
44
+ if field.domain == domain or domain is None:
45
+ array.append(prop)
46
+ return array
47
+
48
+
49
+ def meta_frame(self):
50
+ query_set = MetaFieldDomain.objects.all()
51
+ if query_set.count() == 0:
52
+ query_set = init_meta_frame()
53
+ values = query_set.values('name', 'default__code', 'align')
54
+ return {
55
+ vs['name']: {
56
+ "tool": vs['default__code'],
57
+ "align": vs['align'],
58
+ }
59
+ for vs in values
60
+ }
61
+
62
+
63
+
64
+
65
+ def initial_fields(self, code):
66
+ props = self.props()
67
+ default_keys = meta_field_key_defaults.get(self.entity,{})
68
+ method, array = default_keys.get(code, ('omit',[]))
69
+ def fun(prop): return prop not in array if method == 'omit' else prop in array
70
+ props = [prop for prop in props if fun(prop)]
71
+
72
+ default_values = meta_field_value_defaults.get(self.entity, {})
73
+ init_values = default_values.get('__init__', {})
74
+ code_values = default_values.get(code, {})
75
+ default_fields = always_merger.merge(init_values, code_values)
76
+
77
+ meta_frame = self.meta_frame()
78
+ fields = []
79
+ for prop in props:
80
+ field = self.get_field(prop)
81
+ field_json = field.json()
82
+ frame = meta_frame.get(field.domain, {})
83
+ field_json.update(frame)
84
+ default_field = default_fields.get(prop, {})
85
+ always_merger.merge(field_json, default_field)
86
+ fields.append(field_json)
87
+
88
+ fields.reverse()
89
+ return fields
90
+
91
+ def get_field(self, prop)->OrmField:
92
+ return self.mapping[prop]
93
+
94
+ def detach_item(self, item):
95
+ _id = item.get('id')
96
+ if _id:
97
+ del item['id']
98
+ simple_item = {}
99
+ complex_item = {}
100
+ for prop in item:
101
+ field = self.get_field(prop)
102
+ value = item.get(prop)
103
+ if field.domain in ['ManyToOneRel', 'ManyToManyField', 'ManyToManyRel', 'OneToOneRel', 'OneToOneField','FileField']:
104
+ complex_item[prop] = value
105
+ else:
106
+ simple_item[prop] = value
107
+ return self.object_id(_id), simple_item, complex_item
108
+
109
+ def get_file_paths(self, query_set: QuerySet):
110
+ props = self.props('FileField')
111
+ items = query_set.values(*props)
112
+ array = []
113
+ for item in items:
114
+ array += [i for i in item.values() if i]
115
+ return array
116
+
117
+ def remove_files(self, query_set: QuerySet):
118
+ paths = self.get_file_paths(query_set)
119
+ for path in paths:
120
+ self.minio.remove_path(path)
121
+
122
+ def save_complex_field(self, complex_item, bean):
123
+ for prop in complex_item:
124
+ value = complex_item[prop]
125
+ field = self.get_field(prop).model_field
126
+ clazz = type(field)
127
+ if clazz == ManyToManyField:
128
+ m2m = getattr(bean, prop)
129
+ m2m.clear()
130
+ m2m.add(*value)
131
+ elif clazz == ManyToOneRel:
132
+ getattr(bean, field.get_accessor_name()).clear()
133
+ remote_model: VModel = field.related_model
134
+ new_set: QuerySet = remote_model.objects.filter(id__in=value)
135
+ remote_field: ForeignKey = field.remote_field
136
+ k = remote_field.get_attname()
137
+ new_set.update(**{k: bean.id})
138
+ elif clazz == ManyToManyRel:
139
+ getattr(bean, field.get_accessor_name()).clear()
140
+ remote_model: VModel = field.related_model
141
+ remote_items: QuerySet = remote_model.objects.filter(id__in=value)
142
+ remote_field: ManyToManyField = field.remote_field
143
+ remote_field_prop = remote_field.get_attname()
144
+ for _bean in remote_items:
145
+ bean_set = getattr(_bean, remote_field_prop)
146
+ bean_set.add(bean)
147
+ elif clazz == OneToOneRel:
148
+ remote_model: VModel = field.related_model
149
+ remote_field: OneToOneField = field.remote_field
150
+ remote_field_prop = remote_field.get_attname()
151
+ _bean = remote_model.objects.get(id=value)
152
+ __bean = remote_model.objects.filter(**{remote_field_prop: bean.id}).first()
153
+ if __bean:
154
+ setattr(__bean, remote_field_prop, None)
155
+ __bean.save()
156
+ setattr(_bean, remote_field_prop, bean.id)
157
+ _bean.save()
158
+ elif clazz == OneToOneField:
159
+ __bean = field.model.objects.filter(**{prop: value}).first()
160
+ if __bean:
161
+ setattr(__bean, prop, None)
162
+ __bean.save()
163
+ setattr(bean, prop, value)
164
+ elif clazz == FileField:
165
+ file_name, _bytes = value
166
+ field_file: FieldFile = getattr(bean, prop)
167
+ if field_file:
168
+ path = field_file.name
169
+ self.minio.remove_path(path)
170
+ object_name = self.minio.get_object_name(bean.id, prop, file_name)
171
+ path = self.minio.upload(self.bucket_name, object_name, _bytes) if _bytes else None
172
+ setattr(bean, prop, path)
173
+
174
+
175
+
176
+
177
+ def to_dict(self, query_set: QuerySet, code=None):
178
+ # query_set = query_set.filter(saved=True)
179
+ utils = OrmUtils()
180
+ orm_fields = self.mapping.values()
181
+ # 简单字段取值
182
+ simple_props = [field.prop for field in orm_fields if field.domain not in utils.referred_domains]
183
+ custom_props = utils.custom_props(self.entity, code)
184
+ results = list(query_set.values(*[*simple_props, *custom_props]))
185
+ utils.date_values(orm_fields, results)
186
+ # 关系型字段取值
187
+ mapping = { row['id']: row for row in results}
188
+ referred_fields = [field for field in orm_fields if field.domain in utils.referred_domains]
189
+ for field in referred_fields:
190
+ utils.linkage(field, query_set, mapping)
191
+ return results
192
+
@@ -0,0 +1,12 @@
1
+
2
+ def __translate_finder__(conditions):
3
+ return {}
4
+
5
+
6
+ class MonQuery:
7
+
8
+ def __init__(self, conditions: list, orders = None):
9
+ self.orders = orders or {'sort': -1}
10
+ conditions = conditions if len(conditions) else [{'includes':{},'excludes':{}}]
11
+ self.finder = __translate_finder__(conditions)
12
+
@@ -0,0 +1,43 @@
1
+ from django.db.models import Q
2
+ from functools import reduce
3
+
4
+
5
+
6
+ def __fun__(x, y): return x | y
7
+ def __translate_orders__(orders):
8
+ array = []
9
+ for key in orders:
10
+ value = orders.get(key)
11
+ prefix = '-' if value == -1 else ''
12
+ array.append(f'{prefix}{key}')
13
+ return array
14
+ def __translate_condition__(conditions, _type):
15
+
16
+
17
+
18
+ return reduce(__fun__, [Q(**cond[_type]) for cond in conditions])
19
+
20
+
21
+ class OrmQuery:
22
+
23
+ def __init__(self, conditions: list, orders = None):
24
+ self.orders = __translate_orders__(orders or {'sort': -1})
25
+ conditions = conditions if len(conditions) else [{'includes':{},'excludes':{}}]
26
+ self.includes = __translate_condition__(conditions,'includes')
27
+ self.excludes = __translate_condition__(conditions, 'excludes')
28
+
29
+ @staticmethod
30
+ def is_empty(conditions):
31
+ if conditions and len(conditions):
32
+ temp = {}
33
+ for cond in conditions:
34
+ temp.update(cond)
35
+ return len(temp.keys()) >0
36
+ else:
37
+ return True
38
+
39
+
40
+
41
+
42
+
43
+
@@ -0,0 +1,85 @@
1
+ from ..valar_models import VModel
2
+ from ...core.meta.field_orm import OrmField
3
+ from ...data.models import MetaField
4
+
5
+
6
+ class OrmUtils:
7
+ def __init__(self):
8
+ self.multiple_domains = ['ManyToOneRel', 'ManyToManyField', 'ManyToManyRel']
9
+ self.referred_domains = [*self.multiple_domains, 'OneToOneRel', 'OneToOneField', 'ForeignKey']
10
+ self.omit_field_props = ['create_time', 'modify_time', 'saved', 'sort']
11
+ self.data_props_formatting = {'DateField': '%Y-%m-%d', 'DateTimeField': '%Y-%m-%d %H:%M:%S', 'TimeField': '%H:%M:%S'}
12
+
13
+
14
+ @staticmethod
15
+ def json(bean: VModel):
16
+ pass
17
+
18
+ @staticmethod
19
+ def custom_props(entity, code='default'):
20
+ field_set = MetaField.objects.filter(view__code=code, view__meta__entity=entity, domain='Custom').values('prop')
21
+ return [item['prop'] for item in field_set if item['prop']]
22
+
23
+
24
+ def linkage(self, field, query_set, mapping):
25
+ model_field = field.model_field
26
+ prop = model_field.name
27
+ multiple = field.domain in self.multiple_domains
28
+
29
+ # 获取级联关系的键索引
30
+ ref_prop = f'{prop}__id'
31
+ edges = query_set.exclude(**{f'{ref_prop}__isnull': True}).values('id', ref_prop)
32
+ if multiple:
33
+ related_primary_keys = set()
34
+ results_mapping = {}
35
+ for edge in edges:
36
+ _id, rid = edge['id'], edge[ref_prop]
37
+ related_primary_keys.add(rid)
38
+ array = results_mapping.get(_id, [])
39
+ array.append(rid)
40
+ results_mapping[_id] = array
41
+ else:
42
+ results_mapping = {row['id']: row[ref_prop] for row in edges if row[ref_prop]}
43
+ related_primary_keys = set(results_mapping.values())
44
+
45
+ # 获取级联关系从属方的数据
46
+ related_model = model_field.related_model
47
+ related_fields = related_model._meta.get_fields()
48
+ related_props = self.__get_related_props__(related_fields)
49
+ related_values = list(related_model.objects.filter(id__in=related_primary_keys).values(*related_props))
50
+ self.date_values(related_fields, related_values)
51
+ related_mapping = {item['id']: item for item in related_values}
52
+
53
+ # 将从属方的数据绑定在主数据上
54
+ for _id in mapping:
55
+ row = mapping[_id]
56
+ if multiple:
57
+ keys = results_mapping.get(_id, [])
58
+ items = [related_mapping[pid] for pid in keys]
59
+ row[prop] = keys
60
+ row[f'{prop}_set'] = items
61
+ else:
62
+ key = results_mapping.get(_id)
63
+ item = related_mapping.get(key) if key else None
64
+ row[prop] = item
65
+ row[f'{prop}_id'] = key
66
+
67
+ def __get_related_props__(self,fields):
68
+ def fun(field): return type(field).__name__ not in self.referred_domains and field.name not in self.omit_field_props
69
+ return [field.name for field in fields if fun(field)]
70
+
71
+ def date_values(self, fields, values):
72
+ date_props_mapping = {}
73
+ for field in fields:
74
+ if isinstance(field, OrmField):
75
+ prop = field.prop
76
+ domain = field.domain
77
+ else:
78
+ prop = field.name
79
+ domain = type(field).__name__
80
+ if domain in self.data_props_formatting.keys():
81
+ date_props_mapping[prop] = self.data_props_formatting[domain]
82
+ for row in values:
83
+ for prop, formating in date_props_mapping.items():
84
+ if row.get(prop):
85
+ row[prop] = row[prop].strftime(formating)
@@ -0,0 +1,63 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class AbstractDao(ABC):
4
+
5
+
6
+ @abstractmethod
7
+ def get_model(self):
8
+ pass
9
+
10
+ @abstractmethod
11
+ def save_one(self, item):
12
+ pass
13
+
14
+ @abstractmethod
15
+ def delete_one(self, _id):
16
+ pass
17
+
18
+ @abstractmethod
19
+ def find_one(self, _id):
20
+ pass
21
+
22
+ @abstractmethod
23
+ def find(self, conditions=None, orders=None, size=0, page=1):
24
+ pass
25
+
26
+ @abstractmethod
27
+ def update(self, template, conditions):
28
+ pass
29
+
30
+ @abstractmethod
31
+ def delete(self, conditions):
32
+ pass
33
+
34
+ @abstractmethod
35
+ def transform(self, o, code=None):
36
+ pass
37
+
38
+ @abstractmethod
39
+ def tree(self, root, conditions=None):
40
+ pass
41
+
42
+ def search(self, includes=None, excludes=None, orders=None):
43
+ conditions = [{
44
+ "includes": includes or {},
45
+ "excludes": excludes or {}
46
+ }]
47
+ query_set, _ = self.find(conditions, orders)
48
+ return query_set
49
+
50
+ # @abstractmethod
51
+ # def values(self, props, conditions, orders=None):
52
+ # pass
53
+ #
54
+ # @abstractmethod
55
+ # def group(self, props, conditions, orders=None):
56
+ # pass
57
+ #
58
+ # @abstractmethod
59
+ # def count(self, props, conditions):
60
+ # pass
61
+
62
+
63
+
File without changes
File without changes
@@ -0,0 +1,17 @@
1
+ __mf_common__ = ['prop','name']
2
+
3
+ meta_field_key_defaults = {
4
+ 'data.Meta': {
5
+ 'default': ('pick', ['entity','name']),
6
+ },
7
+ 'data.MetaView': {
8
+ 'list': ('pick', ['meta_id','code','view_name']),
9
+ },
10
+ 'data.MetaField': {
11
+ 'add': ('pick',['prop','domain','name']),
12
+ 'tool': ('pick',[*__mf_common__,'domain','tool','refer','format']),
13
+ 'rest': ('pick',[*__mf_common__,'not_null','allow_edit','allow_sort','allow_search','allow_download','allow_upload','allow_update']),
14
+ 'table': ('pick',[*__mf_common__,'unit','column_width','fixed','align','edit_on_table','hide_on_table','header_color','cell_color']),
15
+ 'form': ('pick',[*__mf_common__,'hide_on_form','hide_on_form_insert','hide_on_form_edit','hide_on_form_branch','hide_on_form_leaf','span']),
16
+ }
17
+ }
@@ -0,0 +1,85 @@
1
+
2
+ meta_field_value_defaults = {
3
+ 'data.MetaFieldDomain':{
4
+ "__init__":{
5
+ "default_id":{
6
+ "tool":"tree"
7
+ },
8
+ "search_id":{
9
+ "tool":"tree"
10
+ },
11
+ "tools":{
12
+ "tool":"tree",
13
+ "refer": {
14
+ "display":"code"
15
+ }
16
+ },
17
+ "align":{
18
+ "tool":"set",
19
+ "format":{
20
+ "set": {
21
+ 'left':'左对齐',
22
+ 'right':'右对齐',
23
+ 'center':'剧中对齐',
24
+ }
25
+ }
26
+ }
27
+ }
28
+ },
29
+ 'data.MetaField':{
30
+ "__init__":{
31
+ "column_width":{
32
+ 'unit':'px'
33
+ },
34
+ "fixed":{
35
+ "tool":"set",
36
+ "format":{
37
+ "set": {
38
+ 'left':'左侧固定',
39
+ 'right':'右侧固定',
40
+ }
41
+ }
42
+ },
43
+ "align":{
44
+ "tool":"set",
45
+ "format":{
46
+ "set": {
47
+ 'left':'左对齐',
48
+ 'right':'右对齐',
49
+ 'center':'剧中对齐',
50
+ }
51
+ }
52
+ },
53
+ "prop":{
54
+ 'allow_edit': False,
55
+ 'column_width': 120
56
+ },
57
+ "domain":{
58
+ 'allow_edit': False,
59
+ 'column_width': 120,
60
+ },
61
+ "tool":{
62
+ 'column_width': 100,
63
+ 'tool': 'tree',
64
+ 'refer': {
65
+ 'entity':'data.MetaFieldTool',
66
+ 'includes': {'metafielddomain__name':'$domain'},
67
+ 'value': 'code'
68
+ }
69
+
70
+ },
71
+ "span":{
72
+ 'column_width': 100,
73
+ "format": { "min": 0, "max": 24, "step": 1, "precision": 0, "step_strictly": True }
74
+ },
75
+ "refer":{
76
+ 'allow_edit': False,
77
+ 'column_width': 80
78
+ },
79
+ "format":{
80
+ 'allow_edit': False,
81
+ 'column_width': 80
82
+ },
83
+ }
84
+ }
85
+ }