codeflowhub 1.2.0__tar.gz → 1.4.0__tar.gz

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 (33) hide show
  1. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/PKG-INFO +1 -1
  2. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/flow.py +46 -1
  3. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/service/airflow_exporter.py +34 -2
  4. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub.egg-info/PKG-INFO +1 -1
  5. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub.egg-info/SOURCES.txt +2 -1
  6. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/setup.py +1 -1
  7. codeflowhub-1.4.0/tests/test_flow_concurrency.py +55 -0
  8. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/LICENSE +0 -0
  9. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/README.md +0 -0
  10. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/__init__.py +0 -0
  11. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/action.py +0 -0
  12. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/airflow/__init__.py +0 -0
  13. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/airflow/xcom.py +0 -0
  14. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/base.py +0 -0
  15. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/model.py +0 -0
  16. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/service/__init__.py +0 -0
  17. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/storage/__init__.py +0 -0
  18. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/storage/local_storage.py +0 -0
  19. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/storage/s3_storage.py +0 -0
  20. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/storage/storage.py +0 -0
  21. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/task.py +0 -0
  22. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/__init__.py +0 -0
  23. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/analyze_speaker_pkg/__init__.py +0 -0
  24. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/analyze_speaker_pkg/main.py +0 -0
  25. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/extract_voice_pkg/__init__.py +0 -0
  26. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/extract_voice_pkg/main.py +0 -0
  27. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/read_pdf_pkg/__init__.py +0 -0
  28. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/read_pdf_pkg/main.py +0 -0
  29. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/transcript_pkg/__init__.py +0 -0
  30. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub/template/transcript_pkg/main.py +0 -0
  31. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub.egg-info/dependency_links.txt +0 -0
  32. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/codeflowhub.egg-info/top_level.txt +0 -0
  33. {codeflowhub-1.2.0 → codeflowhub-1.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: workflow development tools
5
5
  Author: creaddiscans
6
6
  Author-email: creaddiscans@gmail.com
@@ -30,6 +30,9 @@ class FlowDecorator(BaseDecorator):
30
30
  export_destination:str # Export 시 생성 파일 저장 디렉토리
31
31
  default_args:dict # Airflow DAG default_args (owner, retries, retry_delay 등 override)
32
32
  schedule:str # Airflow DAG 스케줄 (cron 문자열 / preset / timedelta / None=수동 트리거)
33
+ doc_md:str # Airflow DAG doc_md (UI에 표시될 Markdown 문서)
34
+ max_active_runs:int # DAG 동시 실행(run) 상한 (None=Airflow 기본값)
35
+ max_active_tasks:int # DAG 내 동시 실행 task 상한 (None=Airflow 기본값)
33
36
  start_date:datetime # Airflow DAG start_date (None이면 export 시점)
34
37
  secrets:list # 모든 task에 공통 주입할 K8s Secret(env) 목록
35
38
  on_failure: 'BaseDecorator' = None # 모든 task의 기본 failure handler
@@ -38,7 +41,8 @@ class FlowDecorator(BaseDecorator):
38
41
  tags=None, annotations=None, service_account_name=None, volumes=None,
39
42
  airflow_sidecar_image=None, airflow_connection_id=None, repo=None, path=None,
40
43
  export_destination='dags', default_args=None, secrets=None, on_failure=None,
41
- schedule=None, start_date=None, schedule_interval=None, **kwargs):
44
+ schedule=None, start_date=None, doc_md=None, max_active_runs=None, max_active_tasks=None,
45
+ schedule_interval=None, **kwargs):
42
46
  # CLI 속성 먼저 초기화 (init()에서 사용됨)
43
47
  self._cli_export = None
44
48
  self._cli_job_dir = None
@@ -65,6 +69,9 @@ class FlowDecorator(BaseDecorator):
65
69
  self._resolve_schedule_alias(schedule, schedule_interval)
66
70
  )
67
71
  self.start_date = start_date
72
+ self.doc_md = self._normalize_doc_md(doc_md)
73
+ self.max_active_runs = self._positive_int('max_active_runs', max_active_runs)
74
+ self.max_active_tasks = self._positive_int('max_active_tasks', max_active_tasks)
68
75
  # Design Ref: §3.2 — flow-level 공통 Secret(env)
69
76
  self.secrets = secrets or []
70
77
  self.on_failure = on_failure
@@ -75,6 +82,44 @@ class FlowDecorator(BaseDecorator):
75
82
  # super().__init__()는 마지막에 호출 (init()이 실행됨)
76
83
  super().__init__(*args, name=name, **kwargs)
77
84
 
85
+ @staticmethod
86
+ def _positive_int(name, value):
87
+ """None 또는 양의 정수만 허용 (선언 시점에 조기 실패)"""
88
+ if value is None:
89
+ return None
90
+ if isinstance(value, bool) or not isinstance(value, int) or value < 1:
91
+ raise ValueError(f'{name}는 1 이상의 정수 또는 None만 가능합니다: {value!r}')
92
+ return value
93
+
94
+ @staticmethod
95
+ def _normalize_doc_md(doc_md):
96
+ """doc_md 정규화 (선언 시점에 조기 실패)
97
+
98
+ - str: Markdown 본문. 함수 안에서 triple-quote로 쓴 경우의 공통 들여쓰기를 제거
99
+ - Path: 해당 파일의 내용을 읽어 사용 (README 등 별도 파일 참조용)
100
+ - None / 빈 문자열: doc_md 미설정
101
+ """
102
+ import textwrap
103
+
104
+ if doc_md is None:
105
+ return None
106
+
107
+ if isinstance(doc_md, Path):
108
+ if not doc_md.exists():
109
+ raise FileNotFoundError(f'doc_md 파일을 찾을 수 없습니다: {doc_md}')
110
+ text = doc_md.read_text(encoding='utf-8')
111
+ elif isinstance(doc_md, str):
112
+ text = doc_md
113
+ else:
114
+ raise TypeError(
115
+ f'doc_md는 str(Markdown 본문), Path(파일 경로), None만 가능합니다. '
116
+ f'전달된 타입: {type(doc_md).__name__}'
117
+ )
118
+
119
+ # 선행 개행 제거 후 dedent → triple-quote 블록의 들여쓰기가 코드블록으로 렌더되는 것 방지
120
+ text = textwrap.dedent(text.lstrip('\n')).rstrip()
121
+ return text or None
122
+
78
123
  @staticmethod
79
124
  def _resolve_schedule_alias(schedule, schedule_interval):
80
125
  """schedule / schedule_interval(구 이름) 중 하나로 정리
@@ -269,13 +269,30 @@ SETUP_SCRIPT = """{setup_script}"""
269
269
  params_str = self._format_dict(self.flow.params, indent=1)
270
270
  params_line = f"\n params={params_str},"
271
271
 
272
- dag_definition = f'''dag = DAG(
272
+ # doc_md 설정 — 본문은 DAG_DOC_MD 변수로 분리 (여러 줄 Markdown)
273
+ doc_md_definition = ""
274
+ doc_md_line = ""
275
+ doc_md = getattr(self.flow, 'doc_md', None)
276
+ if doc_md:
277
+ doc_md_definition = f'''# DAG documentation (Airflow UI)
278
+ DAG_DOC_MD = {self._render_doc_md(doc_md)}
279
+
280
+ '''
281
+ doc_md_line = "\n doc_md=DAG_DOC_MD,"
282
+
283
+ concurrency_lines = ""
284
+ for key in ('max_active_runs', 'max_active_tasks'):
285
+ value = getattr(self.flow, key, None)
286
+ if value is not None:
287
+ concurrency_lines += f"\n {key}={value},"
288
+
289
+ dag_definition = f'''{doc_md_definition}dag = DAG(
273
290
  '{self.dag_id}',
274
291
  default_args={default_args_str},
275
292
  description='{description}',
276
293
  schedule={self._render_schedule(schedule)},
277
294
  catchup=False,
278
- tags={tags_str},{params_line}
295
+ tags={tags_str},{params_line}{doc_md_line}{concurrency_lines}
279
296
  )
280
297
 
281
298
  '''
@@ -881,6 +898,21 @@ base_volume_mounts = [
881
898
  dependencies.append(f"[{deps_str}] >> {task.name}")
882
899
  return dependencies
883
900
 
901
+ def _render_doc_md(self, doc_md):
902
+ """Markdown 본문 → 생성 DAG의 문자열 리터럴 코드
903
+
904
+ 줄 단위로 repr()을 적용해 이어붙인다. triple-quote를 쓰면 본문에 포함된
905
+ 따옴표/백슬래시가 문법 오류를 만들 수 있으므로 repr로 완전히 이스케이프하고,
906
+ 줄마다 나누어 생성 DAG에서도 읽을 수 있게 유지한다.
907
+ """
908
+ lines = doc_md.split('\n')
909
+ if len(lines) == 1:
910
+ return repr(lines[0])
911
+
912
+ rendered = [f" {line + chr(10)!r}" for line in lines[:-1]]
913
+ rendered.append(f" {lines[-1]!r}")
914
+ return '(\n' + '\n'.join(rendered) + '\n)'
915
+
884
916
  def _render_schedule(self, schedule):
885
917
  """schedule → DAG 코드 문자열
886
918
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: workflow development tools
5
5
  Author: creaddiscans
6
6
  Author-email: creaddiscans@gmail.com
@@ -27,4 +27,5 @@ codeflowhub/template/extract_voice_pkg/main.py
27
27
  codeflowhub/template/read_pdf_pkg/__init__.py
28
28
  codeflowhub/template/read_pdf_pkg/main.py
29
29
  codeflowhub/template/transcript_pkg/__init__.py
30
- codeflowhub/template/transcript_pkg/main.py
30
+ codeflowhub/template/transcript_pkg/main.py
31
+ tests/test_flow_concurrency.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='codeflowhub',
5
- version='1.2.0',
5
+ version='1.4.0',
6
6
  description='workflow development tools',
7
7
  author='creaddiscans',
8
8
  author_email='creaddiscans@gmail.com',
@@ -0,0 +1,55 @@
1
+ """max_active_runs / max_active_tasks 검증·렌더 체크. 실행: python tests/test_flow_concurrency.py
2
+ export 는 끝나면 프로세스를 종료하므로 하위 프로세스로 돌린다."""
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+
8
+ ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
9
+ sys.path.insert(0, ROOT)
10
+ from codeflowhub.flow import FlowDecorator # noqa: E402
11
+
12
+ WF = '''
13
+ import sys
14
+ sys.path.insert(0, {root!r})
15
+ from codeflowhub import flow, task
16
+
17
+ @task(image='busybox')
18
+ def t1(args):
19
+ return args
20
+
21
+ @flow(name='concurrency-check', repo='git@example.com:x/y.git', path='.', export_destination={out!r}{kw})
22
+ def wf(args):
23
+ t1(args)
24
+ '''
25
+
26
+
27
+ def _export(kw=''):
28
+ out = tempfile.mkdtemp()
29
+ src = os.path.join(out, 'wf.py')
30
+ with open(src, 'w') as fh:
31
+ fh.write(WF.format(root=ROOT, out=out, kw=kw))
32
+ subprocess.run([sys.executable, src, '--export', 'airflow'], check=True, capture_output=True)
33
+ with open(os.path.join(out, 'concurrency-check_dag.py')) as fh:
34
+ return fh.read()
35
+
36
+
37
+ def main():
38
+ for bad in (0, -1, '8', True, 1.5):
39
+ try:
40
+ FlowDecorator._positive_int('x', bad)
41
+ raise AssertionError(f'not rejected: {bad!r}')
42
+ except ValueError:
43
+ pass
44
+ assert FlowDecorator._positive_int('x', None) is None
45
+ assert FlowDecorator._positive_int('x', 32) == 32
46
+
47
+ dag = _export(', max_active_runs=32, max_active_tasks=8')
48
+ assert 'max_active_runs=32,' in dag and 'max_active_tasks=8,' in dag, dag
49
+ dag = _export()
50
+ assert 'max_active_runs' not in dag and 'max_active_tasks' not in dag, dag
51
+ print('test_flow_concurrency OK')
52
+
53
+
54
+ if __name__ == '__main__':
55
+ main()
File without changes
File without changes
File without changes