deepfos 1.1.60__py3-none-any.whl → 1.1.78__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.
Files changed (68) hide show
  1. deepfos/_version.py +3 -3
  2. deepfos/api/V1_1/models/business_model.py +322 -322
  3. deepfos/api/V1_1/models/dimension.py +1075 -1075
  4. deepfos/api/V1_2/models/dimension.py +1119 -1116
  5. deepfos/api/account.py +1 -0
  6. deepfos/api/app.py +1 -0
  7. deepfos/api/base.py +70 -71
  8. deepfos/api/deep_pipeline.py +1 -1
  9. deepfos/api/deepconnector.py +3 -3
  10. deepfos/api/financial_model.py +12 -0
  11. deepfos/api/models/account.py +130 -130
  12. deepfos/api/models/accounting_engines.py +250 -250
  13. deepfos/api/models/app.py +355 -355
  14. deepfos/api/models/approval_process.py +231 -231
  15. deepfos/api/models/base.py +49 -209
  16. deepfos/api/models/business_model.py +239 -239
  17. deepfos/api/models/consolidation.py +196 -196
  18. deepfos/api/models/consolidation_process.py +31 -31
  19. deepfos/api/models/datatable_mysql.py +78 -78
  20. deepfos/api/models/deep_pipeline.py +20 -9
  21. deepfos/api/models/deepconnector.py +9 -8
  22. deepfos/api/models/deepfos_task.py +118 -118
  23. deepfos/api/models/deepmodel.py +120 -120
  24. deepfos/api/models/dimension.py +613 -610
  25. deepfos/api/models/financial_model.py +691 -663
  26. deepfos/api/models/journal_model.py +120 -120
  27. deepfos/api/models/journal_template.py +185 -185
  28. deepfos/api/models/memory_financial_model.py +131 -131
  29. deepfos/api/models/platform.py +16 -16
  30. deepfos/api/models/python.py +32 -32
  31. deepfos/api/models/reconciliation_engine.py +104 -104
  32. deepfos/api/models/reconciliation_report.py +29 -29
  33. deepfos/api/models/role_strategy.py +213 -213
  34. deepfos/api/models/smartlist.py +86 -86
  35. deepfos/api/models/space.py +312 -312
  36. deepfos/api/models/system.py +299 -297
  37. deepfos/api/models/variable.py +131 -131
  38. deepfos/api/models/workflow.py +290 -270
  39. deepfos/api/platform.py +3 -1
  40. deepfos/api/space.py +1 -0
  41. deepfos/api/system.py +1 -0
  42. deepfos/api/workflow.py +8 -0
  43. deepfos/cache.py +50 -4
  44. deepfos/element/bizmodel.py +2 -2
  45. deepfos/element/deep_pipeline.py +29 -16
  46. deepfos/element/deepconnector.py +36 -1
  47. deepfos/element/deepmodel.py +591 -332
  48. deepfos/element/dimension.py +30 -17
  49. deepfos/element/finmodel.py +542 -101
  50. deepfos/element/journal.py +20 -10
  51. deepfos/element/rolestrategy.py +4 -4
  52. deepfos/element/variable.py +23 -17
  53. deepfos/element/workflow.py +60 -3
  54. deepfos/exceptions/__init__.py +1 -1
  55. deepfos/lib/deepchart.py +14 -13
  56. deepfos/lib/deepux.py +11 -11
  57. deepfos/lib/discovery.py +3 -0
  58. deepfos/lib/filterparser.py +2 -2
  59. deepfos/lib/k8s.py +101 -0
  60. deepfos/lib/msg.py +34 -8
  61. deepfos/lib/serutils.py +34 -9
  62. deepfos/lib/sysutils.py +37 -18
  63. deepfos/lib/utils.py +62 -2
  64. deepfos/options.py +39 -8
  65. {deepfos-1.1.60.dist-info → deepfos-1.1.78.dist-info}/METADATA +7 -7
  66. {deepfos-1.1.60.dist-info → deepfos-1.1.78.dist-info}/RECORD +68 -67
  67. {deepfos-1.1.60.dist-info → deepfos-1.1.78.dist-info}/WHEEL +0 -0
  68. {deepfos-1.1.60.dist-info → deepfos-1.1.78.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,6 @@
1
1
  import uuid
2
2
 
3
- from pydantic import parse_obj_as
4
-
3
+ from deepfos.api.models import compat_parse_obj_as as parse_obj_as
5
4
  from deepfos.element.datatable import get_table_class
6
5
  from deepfos.exceptions import (
7
6
  JournalModelSaveError, JournalModelCheckError, JournalModelPostingError
@@ -123,8 +122,7 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
123
122
  line_df: pd.DataFrame,
124
123
  callback: Union[Dict, CallbackInfo] = None,
125
124
  relation_field: str = 'journal_id',
126
- id_col: str = None,
127
- operate_type: Literal['EDIT', 'ADD'] = 'ADD',
125
+ id_col: str = MAIN_ID,
128
126
  enable_create: bool = True,
129
127
  enable_default_value: bool = True,
130
128
  enable_repeat_check: bool = True,
@@ -132,9 +130,10 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
132
130
  enable_valid_range: bool = True,
133
131
  enable_all_errors: bool = True,
134
132
  enable_need_one_line: bool = True,
133
+ header_operate: Literal['EDIT', 'ADD', 'DELETE_ADD'] = 'ADD',
134
+ line_operate: Literal['EDIT', 'ADD', 'DELETE', 'DELETE_ADD'] = 'ADD',
135
135
  ):
136
136
  errors = set()
137
- is_editing = operate_type == 'EDIT'
138
137
 
139
138
  if head_df.empty:
140
139
  errors.add('凭证头表数据DataFrame不能为空')
@@ -144,6 +143,7 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
144
143
  errors.add('凭证头行表的关联字段relation_field不能为空')
145
144
 
146
145
  self._maybe_raise_errors(errors)
146
+ is_editing = not (header_operate == 'ADD' and line_operate == 'ADD')
147
147
 
148
148
  if is_editing:
149
149
  head_required_fields = [relation_field, '_type', id_col]
@@ -202,7 +202,7 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
202
202
  index=head_data.index
203
203
  )
204
204
  )
205
- head_data = head_data.assign(operateType=operate_type)
205
+ head_data = head_data.assign(operateType=header_operate)
206
206
 
207
207
  # NB: replace twice in case of infer None to nan happened
208
208
  head_df = head_df.replace({None: np.nan})
@@ -228,7 +228,7 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
228
228
  self._maybe_raise_errors(errors)
229
229
 
230
230
  line_data = line_df[line_required_fields]
231
- line_data = line_data.assign(operateType=operate_type)
231
+ line_data = line_data.assign(operateType=line_operate)
232
232
 
233
233
  # generate lineMainId
234
234
  if is_editing:
@@ -302,7 +302,7 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
302
302
  enable_need_one_line: bool = True,
303
303
  sync: bool = True
304
304
  ) -> CommonResultDTO:
305
- """凭证模型数据保存
305
+ """凭证模型数据新增
306
306
 
307
307
  Args:
308
308
  head_df: 凭证头表的数据(字段名与凭证模型上头表的字段名对应)
@@ -472,6 +472,8 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
472
472
  enable_valid_range: bool = True,
473
473
  enable_all_errors: bool = True,
474
474
  enable_need_one_line: bool = True,
475
+ header_operate: Literal['EDIT', 'DELETE_ADD'] = 'EDIT',
476
+ line_operate: Literal['EDIT', 'ADD', 'DELETE', 'DELETE_ADD'] = 'EDIT',
475
477
  ) -> CommonResultDTO:
476
478
  """凭证模型数据更新
477
479
 
@@ -492,6 +494,10 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
492
494
  enable_valid_range: 是否启用有效性范围的校验,默认为True
493
495
  enable_all_errors: 是否启用一次性校验所有规则和数据,默认为True
494
496
  enable_need_one_line: 是否启用凭证行表至少需要一条数据的校验,默认为True
497
+ header_operate: 凭证头表的操作类型,默认为EDIT
498
+ line_operate: 凭证行表的操作类型,默认为EDIT
499
+ 头表EDIT, 行表可以ADD/EDIT/DELETE
500
+ 头表DELETE_ADD, 头表update,行表全删全增,行表只能为DELETE_ADD
495
501
 
496
502
  Returns:
497
503
  接口返回信息(CommonResultDTO的success为true 表示成功,如false 则错误在errors集合里)
@@ -501,14 +507,16 @@ class AsyncJournalModel(ElementBase[JournalModelAPI]):
501
507
  batch = self._gen_batch_payload(
502
508
  head_df=head_df, line_df=line_df,
503
509
  callback=callback, relation_field=relation_field,
504
- id_col=MAIN_ID, operate_type='EDIT',
510
+ id_col=MAIN_ID,
505
511
  enable_create=enable_create,
506
512
  enable_default_value=enable_default_value,
507
513
  enable_repeat_check=enable_repeat_check,
508
514
  enable_required=enable_required,
509
515
  enable_valid_range=enable_valid_range,
510
516
  enable_all_errors=enable_all_errors,
511
- enable_need_one_line=enable_need_one_line
517
+ enable_need_one_line=enable_need_one_line,
518
+ header_operate=header_operate,
519
+ line_operate=line_operate,
512
520
  )
513
521
  resp = await self.async_api.journal_model_data.update(batch)
514
522
 
@@ -815,6 +823,8 @@ class JournalModel(AsyncJournalModel, metaclass=SyncMeta):
815
823
  enable_valid_range: bool = True,
816
824
  enable_all_errors: bool = True,
817
825
  enable_need_one_line: bool = True,
826
+ header_operate: Literal['EDIT', 'DELETE_ADD'] = 'EDIT',
827
+ line_operate: Literal['EDIT', 'ADD', 'DELETE', 'DELETE_ADD'] = 'EDIT',
818
828
  ) -> CommonResultDTO:
819
829
  ...
820
830
 
@@ -24,13 +24,13 @@ __all__ = [
24
24
  # utils
25
25
  class RoleStrategyRecord(BaseModel):
26
26
  #: 用户
27
- users: Optional[List[Union[UserDTO, str]]]
27
+ users: Optional[List[Union[UserDTO, str]]] = None
28
28
  #: 用户组
29
- user_groups: Optional[List[Union[UserGroupListDTO, str]]]
29
+ user_groups: Optional[List[Union[UserGroupListDTO, str]]] = None
30
30
  #: 角色
31
- roles: Optional[List[Union[RoleDetailVO, str]]]
31
+ roles: Optional[List[Union[RoleDetailVO, str]]] = None
32
32
  #: 角色组
33
- role_groups: Optional[List[Union[RoleGroupDetailVO, str]]]
33
+ role_groups: Optional[List[Union[RoleGroupDetailVO, str]]] = None
34
34
  #: 维度表达式
35
35
  dim_expr: List[Optional[str]]
36
36
 
@@ -1,8 +1,10 @@
1
1
  from enum import Enum
2
- from typing import List, Dict, Union, Any, Callable, TYPE_CHECKING
2
+ from typing import List, Dict, Union, Any, Callable, TYPE_CHECKING, Optional
3
3
  import pandas as pd
4
4
  import datetime
5
5
 
6
+ from pydantic import Field
7
+
6
8
  from deepfos.api.models.variable import VariableValueDTO, VariableVO, UpdateVariavlesDTO, ValueTypeMapDTO, \
7
9
  BaseElementDetail
8
10
  from deepfos.api.variable import VariableAPI
@@ -23,13 +25,13 @@ __all__ = [
23
25
 
24
26
 
25
27
  class GlobalVariable(VariableValueDTO):
26
- description: dict = {'zh-cn': None, 'en': None}
28
+ description: Optional[Dict] = {'zh-cn': None, 'en': None}
27
29
  scope: int = 1
28
30
  status: bool = True
29
31
 
30
32
 
31
33
  class UserVariable(VariableValueDTO):
32
- description: dict = {'zh-cn': None, 'en': None}
34
+ description: Optional[Dict] = {'zh-cn': None, 'en': None}
33
35
  scope: int = 2
34
36
  status: bool = True
35
37
 
@@ -72,17 +74,17 @@ class NumberValueMap(ValueTypeMapDTO):
72
74
  #: 变量类型代码
73
75
  valueType: int = VariableTypeId.NUMBER
74
76
  #: 整数长度
75
- maxLen: int = None
77
+ maxLen: Optional[int] = None
76
78
  #: 小数长度
77
- digitLen: int = None
79
+ digitLen: Optional[int] = None
78
80
  #: 允许等于最小值
79
81
  minEqual: bool = True
80
82
  #: 最小值
81
- minimun: int = None
83
+ minimun: Optional[int] = None
82
84
  #: 允许等于最大值
83
85
  maxEqual: bool = True
84
86
  #: 最大值
85
- maximun: int = None
87
+ maximun: Optional[int] = None
86
88
  #: 是否显示为百分比
87
89
  percentage: bool = False
88
90
 
@@ -91,30 +93,30 @@ class SmartListValueMap(ValueTypeMapDTO):
91
93
  #: 变量类型代码
92
94
  valueType: int = VariableTypeId.SML
93
95
  #: 元素详情(由smartlist提取后的元素信息)
94
- elementDetail: BaseElementDetail = None
96
+ elementDetail: Optional[BaseElementDetail] = None
95
97
  #: 值列表元素名称
96
- valueKey: str = None
98
+ valueKey: Optional[str] = None
97
99
  #: 是否选择多个值列表成员
98
100
  selectedMulti: bool = False
99
101
  #: 多选值列表的成员值列表
100
- valueField: List[str] = []
102
+ valueField: List[str] = Field(default_factory=list)
101
103
  #: 多选成员数上限
102
- multipleChoiceLimit: int = None
104
+ multipleChoiceLimit: Optional[int] = None
103
105
 
104
106
 
105
107
  class DimensionValueMap(ValueTypeMapDTO):
106
108
  #: 变量类型代码
107
109
  valueType: int = VariableTypeId.DIM
108
110
  #: 元素详情(由Dimension提取后的元素信息)
109
- elementDetail: BaseElementDetail = None
111
+ elementDetail: Optional[BaseElementDetail] = None
110
112
  #: 维度元素名称
111
- dimensionName: str = None
113
+ dimensionName: Optional[str] = None
112
114
  #: 维度表达式
113
- valueKey: str = None
115
+ valueKey: Optional[str] = None
114
116
  #: 是否选择多个值列表成员
115
117
  selectedMulti: bool = False
116
118
  #: 多选成员数上限
117
- multipleChoiceLimit: int = None
119
+ multipleChoiceLimit: Optional[int] = None
118
120
 
119
121
 
120
122
  class DateValueMap(ValueTypeMapDTO):
@@ -128,11 +130,11 @@ class CustomListValueMap(ValueTypeMapDTO):
128
130
  #: 变量类型代码
129
131
  valueType: int = VariableTypeId.CUSTOM_LIST
130
132
  #: 自定义列表表达式(多个值请用“,”分开。示例: 1,2,3)
131
- valueKey: str = None
133
+ valueKey: Optional[str] = None
132
134
  #: 是否选择多个值列表成员
133
135
  selectedMulti: bool = False
134
136
  #: 多选成员数上限
135
- multipleChoiceLimit: int = None
137
+ multipleChoiceLimit: Optional[int] = None
136
138
 
137
139
 
138
140
  # -----------------------------------------------------------------------------
@@ -332,6 +334,10 @@ class AsyncVariable(ElementBase[VariableAPI]):
332
334
  if obj_hook is not None:
333
335
  return obj_hook(value)
334
336
 
337
+ # 启用维度表达式的维度变量应直接取原值
338
+ if var.valueType == VariableTypeId.DIM and not var.valueTypeMap.selectedMulti:
339
+ return value
340
+
335
341
  if not auto_cast:
336
342
  cast = None
337
343
  elif var.valueType == VariableTypeId.NUMBER:
@@ -2,8 +2,9 @@ from functools import partial
2
2
  from typing import *
3
3
 
4
4
  import pandas as pd
5
- from pydantic import parse_obj_as
6
5
 
6
+ from deepfos.api.models import compat_parse_obj_as as parse_obj_as
7
+ from deepfos.api.models.platform import FileUplodRes
7
8
  from deepfos.api.models.workflow import *
8
9
  from deepfos.api.workflow import WorkFlowAPI
9
10
  from deepfos.element.base import ElementBase, SyncMeta
@@ -295,22 +296,76 @@ class AsyncWorkFlow(ElementBase[WorkFlowAPI]):
295
296
  comment: str = None,
296
297
  file_path: str = None,
297
298
  outcome: str = None,
299
+ extra_res_params: Dict[str, Any] = None,
300
+ attachments: List[Union[Dict[str, str], FileUplodRes]] = None,
298
301
  ) -> bool:
299
302
  """完成任务实例
300
303
 
301
304
  Args:
302
305
  task_id: 任务实例id
303
306
  comment: 备注
304
- file_path: 附件路径
307
+ file_path: 附件路径(工作流版本在V3.0.4.0后弃用,后续版本请使用attachments参数)
305
308
  outcome: 结果选项,在任务可选结果不唯一时,必须提供
309
+ extra_res_params: 完成参数
310
+ attachments: 附件信息列表,
311
+ 内容一般来自文件管理的/files/upload或/files/upload/content响应值
312
+
306
313
 
307
314
  Returns:
308
315
  True: 成功
309
316
  False: 失败
310
317
 
318
+ .. admonition:: 示例
319
+
320
+ .. code-block:: python
321
+
322
+ from deepfos.api.platform import PlatformAPI
323
+ from deepfos.element.workflow import WorkFlow
324
+
325
+ # 上传附件't.txt'
326
+ upload_resp = PlatformAPI().file.upload(
327
+ file_type='DL', file_name='t.txt', file='some text'
328
+ )
329
+
330
+ # 以动作approve,参数{"a": 1, "b": "42"}完成任务实例,
331
+ # 并提供附件信息为上传的't.txt'文件,备注为"Completed by SDK"
332
+ test_task = WorkFlow('test_task')
333
+ test_task.complete_task_by_id(
334
+ task_id='fd94f6a7-3467-47f9-8a3c-ff626e68dcf5',
335
+ outcome='approve',
336
+ extra_res_params={'a': 1, 'b': '42'},
337
+ comment='Completed by SDK',
338
+ attachments=[upload_resp]
339
+ )
340
+
311
341
  """
342
+ action_id = None
343
+ if outcome is not None:
344
+ outcomes = await self.async_api.task.outcomes(task_id)
345
+ for o in outcomes:
346
+ if o.code == outcome:
347
+ action_id = o.id
348
+ break
349
+
350
+ if extra_res_params is None:
351
+ extra_res_params = {}
352
+ if not isinstance(extra_res_params, dict):
353
+ raise TypeError('extra_res_params参数应为字典类型')
354
+
355
+ if attachments is not None:
356
+ attachments = parse_obj_as(List[FileUplodRes], attachments)
357
+ else:
358
+ attachments = []
312
359
  return await self.async_api.task.express_complete(
313
- TaskCompleteInstance(comment=comment, filePath=file_path, outcome=outcome, taskId=task_id)
360
+ TaskCompleteInstance(
361
+ comment=comment, filePath=file_path, outcome=outcome, taskId=task_id,
362
+ extraResParams=[
363
+ TaskCompleteParam(name=k, value=v)
364
+ for k, v in extra_res_params.items()
365
+ ],
366
+ actionId=action_id,
367
+ attachments=attachments
368
+ )
314
369
  )
315
370
 
316
371
  async def list_process(
@@ -536,6 +591,8 @@ class WorkFlow(AsyncWorkFlow, metaclass=SyncMeta):
536
591
  comment: str = None,
537
592
  file_path: str = None,
538
593
  outcome: str = None,
594
+ extra_res_params: Dict[str, Any] = None,
595
+ attachments: List[Union[Dict[str, str], FileUplodRes]] = None,
539
596
  ) -> bool:
540
597
  ...
541
598
 
@@ -200,7 +200,7 @@ class MsgCenterError(DeepfosBaseException):
200
200
  self.reasons = reasons
201
201
 
202
202
  def __str__(self):
203
- return '推送失败,报错信息:' + '\n'.join([r.errorMsg for r in self.reasons])
203
+ return '推送失败,响应错误详情:\n' + '\n'.join([str(r.dict()) for r in self.reasons])
204
204
 
205
205
 
206
206
  # -----------------------------------------------------------------------------
deepfos/lib/deepchart.py CHANGED
@@ -6,8 +6,9 @@ from inspect import isfunction
6
6
  from pandas import DataFrame
7
7
  from typing import *
8
8
  import pandas as pd
9
- from pydantic import Field, parse_obj_as
9
+ from pydantic import Field
10
10
 
11
+ from deepfos.api.models import compat_parse_obj_as as parse_obj_as
11
12
  from deepfos.lib.asynchronous import evloop
12
13
  from deepfos.api.models import BaseModel
13
14
  from deepfos.api.dimension import DimensionAPI
@@ -94,10 +95,10 @@ class LogicInfo(NamedTuple):
94
95
 
95
96
 
96
97
  class BaseField(BaseModel):
97
- code: int = None
98
- columnName: str = None
99
- description: str = None
100
- value_key: str = None
98
+ code: Optional[int] = None
99
+ columnName: Optional[str] = None
100
+ description: Optional[str] = None
101
+ value_key: Optional[str] = None
101
102
 
102
103
  def __init__(self, name: str = None, description: str = None, **data):
103
104
  super().__init__(columnName=name, description=description, **data)
@@ -125,14 +126,14 @@ class BaseField(BaseModel):
125
126
 
126
127
 
127
128
  class Text(BaseField):
128
- code = 1
129
+ code: int = 1
129
130
 
130
131
 
131
132
  class Date(BaseField):
132
- code = 11
133
+ code: int = 11
133
134
  # 前端时间筛选器颗粒度字段;4:最宽泛的颗粒度
134
- value_key = '4'
135
- precision = TimeType.day
135
+ value_key: str = '4'
136
+ precision: TimeType = TimeType.day
136
137
 
137
138
  @property
138
139
  def logic_info(self) -> LogicInfo:
@@ -140,11 +141,11 @@ class Date(BaseField):
140
141
 
141
142
 
142
143
  class Number(BaseField):
143
- code = 15
144
+ code: int = 15
144
145
 
145
146
 
146
147
  class Person(BaseField):
147
- code = 12
148
+ code: int = 12
148
149
 
149
150
 
150
151
  class BoundField(BaseField):
@@ -195,7 +196,7 @@ class BoundField(BaseField):
195
196
 
196
197
 
197
198
  class SmartList(BoundField):
198
- code = 3
199
+ code: int = 3
199
200
  bound_class: Type[ElementBase] = SmlElement
200
201
 
201
202
  async def async_to_dict(self) -> Dict:
@@ -209,7 +210,7 @@ class SmartList(BoundField):
209
210
 
210
211
 
211
212
  class Dimension(BoundField):
212
- code = 8
213
+ code: int = 8
213
214
  bound_class: Type[ElementBase] = DimElement
214
215
 
215
216
 
deepfos/lib/deepux.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """DeepUX组件数据源"""
2
2
  import asyncio
3
3
  import functools
4
- from typing import Dict, Type, List, Union, Tuple
4
+ from typing import Dict, Type, List, Union, Tuple, Optional
5
5
 
6
6
  import numpy as np
7
7
  import pandas as pd
@@ -40,8 +40,8 @@ OBJECT_TYPE = "object[]"
40
40
 
41
41
 
42
42
  class BaseField(BaseModel):
43
- name: str = None
44
- type: str = None
43
+ name: Optional[str] = None
44
+ type: Optional[str] = None
45
45
 
46
46
  def __init__(self, name: str = None, **data):
47
47
  super().__init__(name=name, **data)
@@ -57,7 +57,7 @@ class ScalarField(BaseField):
57
57
 
58
58
  class String(ScalarField):
59
59
  """文本"""
60
- type = 'str'
60
+ type: str = 'str'
61
61
 
62
62
  def fit(self, df: pd.DataFrame, field_name: str):
63
63
  df[field_name] = np.where(
@@ -69,7 +69,7 @@ class String(ScalarField):
69
69
 
70
70
  class Integer(ScalarField):
71
71
  """整数"""
72
- type = 'int'
72
+ type: str = 'int'
73
73
 
74
74
  def fit(self, df: pd.DataFrame, field_name: str):
75
75
  df[field_name] = np.where(
@@ -81,7 +81,7 @@ class Integer(ScalarField):
81
81
 
82
82
  class Boolean(ScalarField):
83
83
  """布尔"""
84
- type = 'bool'
84
+ type: str = 'bool'
85
85
 
86
86
  def fit(self, df: pd.DataFrame, field_name: str):
87
87
  df[field_name] = np.where(
@@ -93,12 +93,12 @@ class Boolean(ScalarField):
93
93
 
94
94
  class Json(ScalarField):
95
95
  """多语言文本(json)"""
96
- type = 'json'
96
+ type: str = 'json'
97
97
 
98
98
 
99
99
  class Float(ScalarField):
100
100
  """小数"""
101
- type = 'float'
101
+ type: str = 'float'
102
102
 
103
103
  def fit(self, df: pd.DataFrame, field_name: str):
104
104
  df[field_name] = np.where(
@@ -110,17 +110,17 @@ class Float(ScalarField):
110
110
 
111
111
  class DateTime(ScalarField):
112
112
  """日期时间"""
113
- type = 'datetime'
113
+ type: str = 'datetime'
114
114
 
115
115
 
116
116
  class UUID(ScalarField):
117
117
  """uuid"""
118
- type = 'uuid'
118
+ type: str = 'uuid'
119
119
 
120
120
 
121
121
  class ObjectField(BaseField):
122
122
  """表示对象类型的结构信息, 图格式数据源专用"""
123
- type = OBJECT_TYPE
123
+ type: str = OBJECT_TYPE
124
124
  fields: List[ScalarField] = Field(default_factory=list)
125
125
 
126
126
 
deepfos/lib/discovery.py CHANGED
@@ -256,6 +256,9 @@ class ServiceDiscovery(ABC):
256
256
  elif impl == 'nacos':
257
257
  from deepfos.lib.nacos import Nacos
258
258
  cls.__ins__[impl] = Nacos
259
+ elif impl == 'k8s':
260
+ from deepfos.lib.k8s import K8s
261
+ cls.__ins__[impl] = K8s
259
262
 
260
263
  return cls.__ins__[impl]
261
264
 
@@ -2,9 +2,9 @@ import collections
2
2
  import re
3
3
  from enum import Enum, IntEnum
4
4
  from typing import *
5
- from pydantic import Field, parse_obj_as
5
+ from pydantic import Field
6
6
 
7
- from deepfos.api.models import BaseModel
7
+ from deepfos.api.models import BaseModel, compat_parse_obj_as as parse_obj_as
8
8
  from deepfos.core.logictable.nodemixin import NodeMixin, TreeRenderer
9
9
  from deepfos.lib.utils import CIEnum, CIEnumMeta
10
10
 
deepfos/lib/k8s.py ADDED
@@ -0,0 +1,101 @@
1
+ import asyncio
2
+ from collections import defaultdict
3
+ from functools import cached_property
4
+ from typing import TypedDict, Literal, Type, Dict, FrozenSet
5
+
6
+ from loguru import logger
7
+ from kubernetes_asyncio import client, config
8
+ from kubernetes_asyncio.client import (
9
+ V1ServiceList,
10
+ V1Endpoints,
11
+ V1EndpointSubset,
12
+ V1EndpointAddress,
13
+ CoreV1EndpointPort,
14
+ )
15
+ from deepfos.lib.discovery import ServiceDiscovery
16
+ from deepfos import OPTION
17
+
18
+
19
+ class K8s(ServiceDiscovery):
20
+ def __init__(self):
21
+ super().__init__()
22
+ self._instance_lock: Dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
23
+ self._server_lock = asyncio.Lock()
24
+ self._cares = set()
25
+ self._ns = OPTION.k8s.namespace
26
+
27
+ async def on_close(self):
28
+ self._cares.clear()
29
+ self.server_cache.clear()
30
+ self._instance_lock.clear()
31
+
32
+ async def on_startup(self):
33
+ config.load_incluster_config()
34
+
35
+ @cached_property
36
+ def _api(self):
37
+ return client.CoreV1Api()
38
+
39
+ async def on_interval(self):
40
+ await self._update_cache()
41
+
42
+ async def _update_cache(self):
43
+ logger.opt(lazy=True).debug(f"Update cache for instance: {self._cares}")
44
+ await asyncio.gather(*(
45
+ self.update_instance_for_service(sn)
46
+ for sn in self._cares
47
+ ))
48
+
49
+ async def update_service_cache(self, server_name: str):
50
+ async with self._server_lock:
51
+ if server_name not in self.server_cache:
52
+ await self.update_services()
53
+
54
+ async def update_instance_cache(self, server_name):
55
+ self._cares.add(server_name)
56
+ await self.update_instance_for_service(server_name)
57
+
58
+
59
+ async def _list_all_service_names(self) -> FrozenSet[str]:
60
+ svcs: V1ServiceList = await self._api.list_namespaced_service(self._ns)
61
+ return frozenset(svc.metadata.name for svc in svcs.items)
62
+
63
+ async def update_services(self):
64
+ new_services = await self._list_all_service_names()
65
+ cur_services = frozenset(self.server_cache.keys())
66
+
67
+ if added := new_services - cur_services:
68
+ for srv in added:
69
+ self.server_cache.__getitem__(srv)
70
+ logger.debug(f"Added services: {added}")
71
+
72
+ if removed := cur_services - new_services:
73
+ for srv in removed:
74
+ self.server_cache.pop(srv)
75
+ self._instance_lock.pop(srv, None) # noqa
76
+ logger.debug(f"Removed servieces: {removed}")
77
+
78
+ async def update_instance_for_service(self, server_name: str):
79
+ endpoints: V1Endpoints = await self._api.read_namespaced_endpoints(
80
+ server_name,
81
+ self._ns,
82
+ )
83
+
84
+ no_active_nodes = True
85
+ async with self._instance_lock[server_name]:
86
+ cache = self.server_cache[server_name]
87
+
88
+ for subset in endpoints.subsets or []:
89
+ subset: V1EndpointSubset
90
+ for addr in subset.addresses or []:
91
+ addr: V1EndpointAddress
92
+ no_active_nodes = False
93
+ for port in subset.ports:
94
+ port: CoreV1EndpointPort
95
+ url = f"{port.name}://{addr.ip}:{port.port}"
96
+ cache.add(url)
97
+
98
+ if no_active_nodes:
99
+ logger.debug(f"{server_name} has no active endpoint, clear invalid...")
100
+ for item in list(cache):
101
+ cache.delete(item)