codeflowhub 1.1.0__tar.gz → 1.3.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 (32) hide show
  1. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/PKG-INFO +1 -1
  2. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/flow.py +137 -8
  3. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/service/airflow_exporter.py +57 -8
  4. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub.egg-info/PKG-INFO +1 -1
  5. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/setup.py +1 -1
  6. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/LICENSE +0 -0
  7. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/README.md +0 -0
  8. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/__init__.py +0 -0
  9. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/action.py +0 -0
  10. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/airflow/__init__.py +0 -0
  11. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/airflow/xcom.py +0 -0
  12. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/base.py +0 -0
  13. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/model.py +0 -0
  14. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/service/__init__.py +0 -0
  15. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/storage/__init__.py +0 -0
  16. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/storage/local_storage.py +0 -0
  17. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/storage/s3_storage.py +0 -0
  18. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/storage/storage.py +0 -0
  19. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/task.py +0 -0
  20. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/__init__.py +0 -0
  21. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/analyze_speaker_pkg/__init__.py +0 -0
  22. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/analyze_speaker_pkg/main.py +0 -0
  23. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/extract_voice_pkg/__init__.py +0 -0
  24. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/extract_voice_pkg/main.py +0 -0
  25. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/read_pdf_pkg/__init__.py +0 -0
  26. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/read_pdf_pkg/main.py +0 -0
  27. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/transcript_pkg/__init__.py +0 -0
  28. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub/template/transcript_pkg/main.py +0 -0
  29. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub.egg-info/SOURCES.txt +0 -0
  30. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub.egg-info/dependency_links.txt +0 -0
  31. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/codeflowhub.egg-info/top_level.txt +0 -0
  32. {codeflowhub-1.1.0 → codeflowhub-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: workflow development tools
5
5
  Author: creaddiscans
6
6
  Author-email: creaddiscans@gmail.com
@@ -3,6 +3,7 @@ import inspect
3
3
  import json
4
4
  import os
5
5
  import sys
6
+ from datetime import datetime
6
7
  from pathlib import Path
7
8
 
8
9
  from .base import BaseDecorator
@@ -28,13 +29,17 @@ class FlowDecorator(BaseDecorator):
28
29
  path:str # Git repo 내 작업 경로
29
30
  export_destination:str # Export 시 생성 파일 저장 디렉토리
30
31
  default_args:dict # Airflow DAG default_args (owner, retries, retry_delay 등 override)
32
+ schedule:str # Airflow DAG 스케줄 (cron 문자열 / preset / timedelta / None=수동 트리거)
33
+ doc_md:str # Airflow DAG doc_md (UI에 표시될 Markdown 문서)
34
+ start_date:datetime # Airflow DAG start_date (None이면 export 시점)
31
35
  secrets:list # 모든 task에 공통 주입할 K8s Secret(env) 목록
32
36
  on_failure: 'BaseDecorator' = None # 모든 task의 기본 failure handler
33
37
 
34
38
  def __init__(self, *args, namespace='default', env=None, name=None, description=None, params=None,
35
39
  tags=None, annotations=None, service_account_name=None, volumes=None,
36
40
  airflow_sidecar_image=None, airflow_connection_id=None, repo=None, path=None,
37
- export_destination='dags', default_args=None, secrets=None, on_failure=None, **kwargs):
41
+ export_destination='dags', default_args=None, secrets=None, on_failure=None,
42
+ schedule=None, start_date=None, doc_md=None, schedule_interval=None, **kwargs):
38
43
  # CLI 속성 먼저 초기화 (init()에서 사용됨)
39
44
  self._cli_export = None
40
45
  self._cli_job_dir = None
@@ -57,6 +62,11 @@ class FlowDecorator(BaseDecorator):
57
62
  self.path = path
58
63
  self.export_destination = export_destination or 'dags'
59
64
  self.default_args = default_args or {}
65
+ self.schedule = self._normalize_schedule(
66
+ self._resolve_schedule_alias(schedule, schedule_interval)
67
+ )
68
+ self.start_date = start_date
69
+ self.doc_md = self._normalize_doc_md(doc_md)
60
70
  # Design Ref: §3.2 — flow-level 공통 Secret(env)
61
71
  self.secrets = secrets or []
62
72
  self.on_failure = on_failure
@@ -67,6 +77,109 @@ class FlowDecorator(BaseDecorator):
67
77
  # super().__init__()는 마지막에 호출 (init()이 실행됨)
68
78
  super().__init__(*args, name=name, **kwargs)
69
79
 
80
+ @staticmethod
81
+ def _normalize_doc_md(doc_md):
82
+ """doc_md 정규화 (선언 시점에 조기 실패)
83
+
84
+ - str: Markdown 본문. 함수 안에서 triple-quote로 쓴 경우의 공통 들여쓰기를 제거
85
+ - Path: 해당 파일의 내용을 읽어 사용 (README 등 별도 파일 참조용)
86
+ - None / 빈 문자열: doc_md 미설정
87
+ """
88
+ import textwrap
89
+
90
+ if doc_md is None:
91
+ return None
92
+
93
+ if isinstance(doc_md, Path):
94
+ if not doc_md.exists():
95
+ raise FileNotFoundError(f'doc_md 파일을 찾을 수 없습니다: {doc_md}')
96
+ text = doc_md.read_text(encoding='utf-8')
97
+ elif isinstance(doc_md, str):
98
+ text = doc_md
99
+ else:
100
+ raise TypeError(
101
+ f'doc_md는 str(Markdown 본문), Path(파일 경로), None만 가능합니다. '
102
+ f'전달된 타입: {type(doc_md).__name__}'
103
+ )
104
+
105
+ # 선행 개행 제거 후 dedent → triple-quote 블록의 들여쓰기가 코드블록으로 렌더되는 것 방지
106
+ text = textwrap.dedent(text.lstrip('\n')).rstrip()
107
+ return text or None
108
+
109
+ @staticmethod
110
+ def _resolve_schedule_alias(schedule, schedule_interval):
111
+ """schedule / schedule_interval(구 이름) 중 하나로 정리
112
+
113
+ Airflow 2.4+에서 DAG 파라미터가 schedule로 통합되었고 Airflow 3에서
114
+ schedule_interval은 제거되었다. 구 이름도 계속 받되 경고한다.
115
+ """
116
+ if schedule_interval is None:
117
+ return schedule
118
+ if schedule is not None:
119
+ raise ValueError(
120
+ 'schedule과 schedule_interval을 동시에 지정할 수 없습니다. '
121
+ 'schedule만 사용하세요 (schedule_interval은 구 이름).'
122
+ )
123
+ print(
124
+ "⚠️ schedule_interval은 구 이름입니다. Airflow 2.4+ 표준인 "
125
+ "schedule을 사용하세요.",
126
+ file=sys.stderr
127
+ )
128
+ return schedule_interval
129
+
130
+ @staticmethod
131
+ def _normalize_schedule(schedule):
132
+ """schedule 검증 (선언 시점에 조기 실패)
133
+
134
+ 허용: None(수동 트리거), cron 문자열('0 2 * * *'), preset('@daily'), timedelta
135
+ """
136
+ from datetime import timedelta
137
+
138
+ if schedule is None or isinstance(schedule, timedelta):
139
+ return schedule
140
+
141
+ if not isinstance(schedule, str):
142
+ raise TypeError(
143
+ f'schedule은 str(cron/preset), timedelta, None만 가능합니다. '
144
+ f'전달된 타입: {type(schedule).__name__}'
145
+ )
146
+
147
+ value = schedule.strip()
148
+ if not value:
149
+ return None
150
+
151
+ presets = {'@once', '@hourly', '@daily', '@weekly', '@monthly',
152
+ '@quarterly', '@yearly', '@annually', '@continuous', '@none'}
153
+ if value.startswith('@'):
154
+ if value not in presets:
155
+ raise ValueError(
156
+ f'알 수 없는 스케줄 preset: "{value}". '
157
+ f'사용 가능: {", ".join(sorted(presets))}'
158
+ )
159
+ return None if value == '@none' else value
160
+
161
+ # cron 표현식은 5개 필드 (분 시 일 월 요일)
162
+ fields = value.split()
163
+ if len(fields) != 5:
164
+ raise ValueError(
165
+ f'cron 표현식은 5개 필드(분 시 일 월 요일)여야 합니다. '
166
+ f'전달된 값: "{value}" ({len(fields)}개 필드)'
167
+ )
168
+ return value
169
+
170
+ def _resolve_start_date(self):
171
+ """DAG start_date 결정 (Airflow는 스케줄이 있으면 start_date 필수)
172
+
173
+ 우선순위: start_date 파라미터 > default_args['start_date'] > 고정 기본값.
174
+ 기본값을 datetime.now()가 아닌 고정값으로 두어 재export 시 결정적이도록 한다.
175
+ """
176
+ if self.start_date is not None:
177
+ return self.start_date
178
+ from_default_args = (self.default_args or {}).get('start_date')
179
+ if from_default_args is not None:
180
+ return from_default_args
181
+ return datetime(2024, 1, 1)
182
+
70
183
  def _initialize_env(self, env):
71
184
  """환경 설정 초기화"""
72
185
  if env and isinstance(env, dict) and all(isinstance(v, dict) for v in env.values()):
@@ -258,28 +371,33 @@ class FlowDecorator(BaseDecorator):
258
371
  print(f' Supported: airflow')
259
372
  os._exit(1)
260
373
 
261
- from datetime import datetime, timedelta
374
+ from datetime import timedelta
262
375
 
263
376
  flow_id = self.flow_name or self.name
264
377
  dest_dir = self.export_destination or 'dags'
265
378
  print(f'🚀 Exporting {flow_id} to Airflow DAG... (destination: {dest_dir})')
266
379
 
380
+ # 스케줄이 설정된 DAG는 start_date가 필수이므로 default_args에 항상 포함시킨다
381
+ start_date = self._resolve_start_date()
267
382
  default_args = {
268
383
  'owner': 'flowhub',
269
384
  'retries': 1,
270
385
  'retry_delay': timedelta(minutes=5),
271
386
  **(self.default_args or {}),
387
+ 'start_date': start_date,
272
388
  }
273
389
 
274
390
  dag_path = self.export_airflow(
275
391
  output_path=os.path.join(dest_dir, f'{flow_id}_dag.py'),
276
- schedule_interval=None,
277
- start_date=datetime.now(),
392
+ schedule=self.schedule,
393
+ start_date=start_date,
278
394
  default_args=default_args,
279
395
  )
280
396
 
281
397
  print(f'✅ Successfully exported to: {dag_path}')
282
398
  print(f'📋 DAG ID: {flow_id}')
399
+ print(f'⏰ Schedule: {self.schedule or "None (manual trigger)"}')
400
+ print(f'📅 Start date: {start_date.isoformat()}')
283
401
  print(f'🏷️ Tags: [{self.namespace}]')
284
402
  os._exit(0)
285
403
 
@@ -576,23 +694,34 @@ class FlowDecorator(BaseDecorator):
576
694
  f'--input-data를 제공하거나 먼저 전체 workflow를 실행하세요.'
577
695
  )
578
696
 
579
- def export_airflow(self, output_path=None, schedule_interval=None, start_date=None, default_args=None):
697
+ def export_airflow(self, output_path=None, schedule=None, start_date=None, default_args=None,
698
+ schedule_interval=None):
580
699
  """
581
700
  Airflow DAG로 export
582
701
 
583
702
  Args:
584
703
  output_path: DAG 파일 저장 경로 (기본: dags/{dag_id}.py)
585
- schedule_interval: DAG 스케줄 (기본: None)
586
- start_date: DAG 시작 날짜 (기본: 오늘)
704
+ schedule: DAG 스케줄. 미지정 시 @flow(schedule=...) 값 사용
705
+ start_date: DAG 시작 날짜. 미지정 시 @flow(start_date=...) 또는 기본값 사용
587
706
  default_args: DAG default_args
707
+ schedule_interval: schedule의 구 이름 (deprecated)
588
708
 
589
709
  Returns:
590
710
  str: 생성된 DAG 파일 경로
591
711
  """
712
+ # 인자 미지정 시 flow 데코레이터에 선언된 값으로 폴백
713
+ schedule = self._resolve_schedule_alias(schedule, schedule_interval)
714
+ if schedule is None:
715
+ schedule = self.schedule
716
+ else:
717
+ schedule = self._normalize_schedule(schedule)
718
+ if start_date is None:
719
+ start_date = self._resolve_start_date()
720
+
592
721
  exporter = AirflowExporter(self)
593
722
  return exporter.export(
594
723
  output_path=output_path,
595
- schedule_interval=schedule_interval,
724
+ schedule=schedule,
596
725
  start_date=start_date,
597
726
  default_args=default_args
598
727
  )
@@ -149,7 +149,7 @@ class AirflowExporter:
149
149
 
150
150
  def export(self,
151
151
  output_path: str = None,
152
- schedule_interval: Optional[str] = None,
152
+ schedule: Optional[str] = None,
153
153
  start_date: Optional[datetime] = None,
154
154
  default_args: Optional[dict] = None):
155
155
  """Airflow DAG Python 파일로 export"""
@@ -178,7 +178,7 @@ class AirflowExporter:
178
178
  configmap_path = self._generate_configmap_yaml(dir_path)
179
179
 
180
180
  # DAG 코드 생성
181
- dag_code = self._generate_dag_code(schedule_interval, start_date, default_args)
181
+ dag_code = self._generate_dag_code(schedule, start_date, default_args)
182
182
 
183
183
  # 파일 저장
184
184
  with open(output_path, 'w', encoding='utf-8') as f:
@@ -192,7 +192,7 @@ class AirflowExporter:
192
192
  print(f'📋 Apply ConfigMap: kubectl apply -f {configmap_path}')
193
193
  return output_path
194
194
 
195
- def _generate_dag_code(self, schedule_interval, start_date, default_args):
195
+ def _generate_dag_code(self, schedule, start_date, default_args):
196
196
  """DAG Python 코드 생성 (target.py 스타일)"""
197
197
 
198
198
  flow_name = self.flow.flow_name or self.flow.name
@@ -269,13 +269,24 @@ 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
+ dag_definition = f'''{doc_md_definition}dag = DAG(
273
284
  '{self.dag_id}',
274
285
  default_args={default_args_str},
275
286
  description='{description}',
276
- schedule_interval={repr(schedule_interval)},
287
+ schedule={self._render_schedule(schedule)},
277
288
  catchup=False,
278
- tags={tags_str},{params_line}
289
+ tags={tags_str},{params_line}{doc_md_line}
279
290
  )
280
291
 
281
292
  '''
@@ -881,6 +892,32 @@ base_volume_mounts = [
881
892
  dependencies.append(f"[{deps_str}] >> {task.name}")
882
893
  return dependencies
883
894
 
895
+ def _render_doc_md(self, doc_md):
896
+ """Markdown 본문 → 생성 DAG의 문자열 리터럴 코드
897
+
898
+ 줄 단위로 repr()을 적용해 이어붙인다. triple-quote를 쓰면 본문에 포함된
899
+ 따옴표/백슬래시가 문법 오류를 만들 수 있으므로 repr로 완전히 이스케이프하고,
900
+ 줄마다 나누어 생성 DAG에서도 읽을 수 있게 유지한다.
901
+ """
902
+ lines = doc_md.split('\n')
903
+ if len(lines) == 1:
904
+ return repr(lines[0])
905
+
906
+ rendered = [f" {line + chr(10)!r}" for line in lines[:-1]]
907
+ rendered.append(f" {lines[-1]!r}")
908
+ return '(\n' + '\n'.join(rendered) + '\n)'
909
+
910
+ def _render_schedule(self, schedule):
911
+ """schedule → DAG 코드 문자열
912
+
913
+ repr(timedelta)는 'datetime.timedelta(...)'를 만들지만 생성 DAG는
914
+ `from datetime import timedelta`만 import하므로 NameError가 된다.
915
+ """
916
+ if isinstance(schedule, timedelta):
917
+ total = int(schedule.total_seconds())
918
+ return f'timedelta(seconds={total})'
919
+ return repr(schedule)
920
+
884
921
  def _format_dict(self, d, indent=0, base_indent=0):
885
922
  """Dict를 Python 코드 형식으로 포맷팅"""
886
923
  return self._format_dict_with_raw_values(d, indent, raw_keys=set(), base_indent=base_indent)
@@ -908,9 +945,21 @@ base_volume_mounts = [
908
945
  elif isinstance(value, str):
909
946
  items.append(f"'{key}': '{value}'")
910
947
  elif isinstance(value, datetime):
911
- items.append(f"'{key}': datetime({value.year}, {value.month}, {value.day})")
948
+ # 시/분/초가 있으면 함께 렌더 (start_date 정밀도 손실 방지)
949
+ if (value.hour, value.minute, value.second) == (0, 0, 0):
950
+ items.append(f"'{key}': datetime({value.year}, {value.month}, {value.day})")
951
+ else:
952
+ items.append(
953
+ f"'{key}': datetime({value.year}, {value.month}, {value.day}, "
954
+ f"{value.hour}, {value.minute}, {value.second})"
955
+ )
912
956
  elif isinstance(value, timedelta):
913
- items.append(f"'{key}': timedelta(minutes={int(value.total_seconds() / 60)})")
957
+ # 분 단위로 나누어떨어지지 않는 값은 초 단위로 렌더 (절삭 방지)
958
+ total = int(value.total_seconds())
959
+ if total % 60 == 0:
960
+ items.append(f"'{key}': timedelta(minutes={total // 60})")
961
+ else:
962
+ items.append(f"'{key}': timedelta(seconds={total})")
914
963
  elif isinstance(value, bool):
915
964
  items.append(f"'{key}': {value}")
916
965
  elif isinstance(value, (int, float)):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: workflow development tools
5
5
  Author: creaddiscans
6
6
  Author-email: creaddiscans@gmail.com
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='codeflowhub',
5
- version='1.1.0',
5
+ version='1.3.0',
6
6
  description='workflow development tools',
7
7
  author='creaddiscans',
8
8
  author_email='creaddiscans@gmail.com',
File without changes
File without changes
File without changes