codeflowhub 0.2.4__tar.gz → 0.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 (32) hide show
  1. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/PKG-INFO +1 -1
  2. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/flow.py +7 -3
  3. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/service/airflow_exporter.py +103 -1
  4. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/task.py +4 -1
  5. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub.egg-info/PKG-INFO +1 -1
  6. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/setup.py +1 -1
  7. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/LICENSE +0 -0
  8. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/README.md +0 -0
  9. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/__init__.py +0 -0
  10. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/action.py +0 -0
  11. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/airflow/__init__.py +0 -0
  12. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/airflow/xcom.py +0 -0
  13. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/base.py +0 -0
  14. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/model.py +0 -0
  15. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/service/__init__.py +0 -0
  16. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/storage/__init__.py +0 -0
  17. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/storage/local_storage.py +0 -0
  18. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/storage/s3_storage.py +0 -0
  19. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/storage/storage.py +0 -0
  20. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/__init__.py +0 -0
  21. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/analyze_speaker_pkg/__init__.py +0 -0
  22. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/analyze_speaker_pkg/main.py +0 -0
  23. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/extract_voice_pkg/__init__.py +0 -0
  24. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/extract_voice_pkg/main.py +0 -0
  25. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/read_pdf_pkg/__init__.py +0 -0
  26. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/read_pdf_pkg/main.py +0 -0
  27. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/transcript_pkg/__init__.py +0 -0
  28. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub/template/transcript_pkg/main.py +0 -0
  29. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub.egg-info/SOURCES.txt +0 -0
  30. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub.egg-info/dependency_links.txt +0 -0
  31. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/codeflowhub.egg-info/top_level.txt +0 -0
  32. {codeflowhub-0.2.4 → codeflowhub-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: workflow development tools
5
5
  Author: creaddiscans
6
6
  Author-email: creaddiscans@gmail.com
@@ -26,11 +26,13 @@ class FlowDecorator(BaseDecorator):
26
26
  airflow_connection_id:str # Airflow Connection ID for XCom API fetch
27
27
  repo:str # Git repository URL
28
28
  path:str # Git repo 내 작업 경로
29
+ export_destination:str # Export 시 생성 파일 저장 디렉토리
29
30
  on_failure: 'BaseDecorator' = None # 모든 task의 기본 failure handler
30
31
 
31
32
  def __init__(self, *args, namespace='default', env=None, name=None, description=None, params=None,
32
33
  tags=None, annotations=None, service_account_name=None, volumes=None,
33
- airflow_sidecar_image=None, airflow_connection_id=None, repo=None, path=None, on_failure=None, **kwargs):
34
+ airflow_sidecar_image=None, airflow_connection_id=None, repo=None, path=None,
35
+ export_destination='dags', on_failure=None, **kwargs):
34
36
  # CLI 속성 먼저 초기화 (init()에서 사용됨)
35
37
  self._cli_export = None
36
38
  self._cli_job_dir = None
@@ -51,6 +53,7 @@ class FlowDecorator(BaseDecorator):
51
53
  self.airflow_connection_id = airflow_connection_id
52
54
  self.repo = repo
53
55
  self.path = path
56
+ self.export_destination = export_destination or 'dags'
54
57
  self.on_failure = on_failure
55
58
 
56
59
  self._initialize_env(env)
@@ -253,10 +256,11 @@ class FlowDecorator(BaseDecorator):
253
256
  from datetime import datetime, timedelta
254
257
 
255
258
  flow_id = self.flow_name or self.name
256
- print(f'🚀 Exporting {flow_id} to Airflow DAG...')
259
+ dest_dir = self.export_destination or 'dags'
260
+ print(f'🚀 Exporting {flow_id} to Airflow DAG... (destination: {dest_dir})')
257
261
 
258
262
  dag_path = self.export_airflow(
259
- output_path=f'dags/{flow_id}_dag.py',
263
+ output_path=os.path.join(dest_dir, f'{flow_id}_dag.py'),
260
264
  schedule_interval=None,
261
265
  start_date=datetime.now(),
262
266
  default_args={
@@ -283,6 +283,7 @@ base_volume_mounts = [
283
283
 
284
284
  tolerations_code = self._build_tolerations_code(task)
285
285
  node_selector_code = self._build_node_selector_code(task)
286
+ affinity_code = self._build_affinity_code(task)
286
287
  volume_mounts_code = self._build_volume_mounts_code(task)
287
288
  container_resources_code = self._build_container_resources_code(task)
288
289
  sidecars_code = self._build_sidecars_code(task)
@@ -304,7 +305,7 @@ base_volume_mounts = [
304
305
  operator_code = f''' {task.name} = KubernetesPodOperator(
305
306
  **common,
306
307
  task_id='{task.name}',
307
- image='{task_image}',{pool_code}{trigger_rule_code}{retries_code}{env_vars_code}{tolerations_code}{node_selector_code}{volume_mounts_code}{container_resources_code}{sidecars_code}
308
+ image='{task_image}',{pool_code}{trigger_rule_code}{retries_code}{env_vars_code}{tolerations_code}{node_selector_code}{affinity_code}{volume_mounts_code}{container_resources_code}{sidecars_code}
308
309
  arguments=[
309
310
  f\'\'\'{arguments}\'\'\'
310
311
  ],
@@ -361,6 +362,107 @@ base_volume_mounts = [
361
362
  return f"\n node_selector={repr(task.node_selector)},"
362
363
  return ""
363
364
 
365
+ def _build_affinity_code(self, task):
366
+ """Affinity 코드 생성 (node_affinity 지원)
367
+
368
+ affinity dict는 K8s YAML 구조를 그대로 따르며, camelCase/snake_case 모두 허용.
369
+ 예:
370
+ affinity={
371
+ 'node_affinity': {
372
+ 'required_during_scheduling_ignored_during_execution': {
373
+ 'node_selector_terms': [
374
+ {'match_expressions': [
375
+ {'key': 'workload-type/spot', 'operator': 'In', 'values': ['true']}
376
+ ]}
377
+ ]
378
+ },
379
+ 'preferred_during_scheduling_ignored_during_execution': [
380
+ {'weight': 100, 'preference': {
381
+ 'match_expressions': [
382
+ {'key': 'workload-type/spot', 'operator': 'In', 'values': ['true']}
383
+ ]
384
+ }}
385
+ ]
386
+ }
387
+ }
388
+ """
389
+ if not (hasattr(task, 'affinity') and task.affinity):
390
+ return ""
391
+
392
+ aff = self._normalize_keys(task.affinity)
393
+ parts = []
394
+ if 'node_affinity' in aff:
395
+ node_aff_code = self._render_node_affinity(aff['node_affinity'])
396
+ if node_aff_code:
397
+ parts.append(f"node_affinity={node_aff_code}")
398
+
399
+ if not parts:
400
+ return ""
401
+
402
+ return f"\n affinity=k8s.V1Affinity({', '.join(parts)}),"
403
+
404
+ def _render_node_affinity(self, node_aff):
405
+ """k8s.V1NodeAffinity(...) 코드 생성"""
406
+ na = self._normalize_keys(node_aff)
407
+ parts = []
408
+
409
+ if 'required_during_scheduling_ignored_during_execution' in na:
410
+ required = self._normalize_keys(na['required_during_scheduling_ignored_during_execution'])
411
+ terms = required.get('node_selector_terms', [])
412
+ terms_code = ', '.join(self._render_node_selector_term(t) for t in terms)
413
+ parts.append(
414
+ f"required_during_scheduling_ignored_during_execution="
415
+ f"k8s.V1NodeSelector(node_selector_terms=[{terms_code}])"
416
+ )
417
+
418
+ if 'preferred_during_scheduling_ignored_during_execution' in na:
419
+ prefs = na['preferred_during_scheduling_ignored_during_execution'] or []
420
+ pref_codes = []
421
+ for pref in prefs:
422
+ p = self._normalize_keys(pref)
423
+ weight = p.get('weight', 1)
424
+ preference = self._render_node_selector_term(p.get('preference', {}))
425
+ pref_codes.append(
426
+ f"k8s.V1PreferredSchedulingTerm(weight={weight}, preference={preference})"
427
+ )
428
+ parts.append(
429
+ f"preferred_during_scheduling_ignored_during_execution=[{', '.join(pref_codes)}]"
430
+ )
431
+
432
+ return f"k8s.V1NodeAffinity({', '.join(parts)})"
433
+
434
+ def _render_node_selector_term(self, term):
435
+ """k8s.V1NodeSelectorTerm(...) 코드 생성"""
436
+ t = self._normalize_keys(term)
437
+ parts = []
438
+
439
+ for field_key, api_key in [('match_expressions', 'match_expressions'),
440
+ ('match_fields', 'match_fields')]:
441
+ if field_key in t:
442
+ req_codes = [self._render_node_selector_requirement(r) for r in t[field_key]]
443
+ parts.append(f"{api_key}=[{', '.join(req_codes)}]")
444
+
445
+ return f"k8s.V1NodeSelectorTerm({', '.join(parts)})"
446
+
447
+ def _render_node_selector_requirement(self, req):
448
+ """k8s.V1NodeSelectorRequirement(...) 코드 생성"""
449
+ r = self._normalize_keys(req)
450
+ req_parts = [f"key={repr(r['key'])}", f"operator={repr(r['operator'])}"]
451
+ if 'values' in r:
452
+ req_parts.append(f"values={repr(list(r['values']))}")
453
+ return f"k8s.V1NodeSelectorRequirement({', '.join(req_parts)})"
454
+
455
+ @staticmethod
456
+ def _normalize_keys(d):
457
+ """dict의 최상위 키를 camelCase → snake_case로 정규화"""
458
+ import re
459
+ if not isinstance(d, dict):
460
+ return d
461
+ return {
462
+ re.sub(r'([A-Z])', r'_\1', k).lower().lstrip('_'): v
463
+ for k, v in d.items()
464
+ }
465
+
364
466
  def _build_volume_mounts_code(self, task, include_base=True):
365
467
  """Volume mounts 코드 생성
366
468
 
@@ -13,11 +13,13 @@ class TaskDecorator(BaseDecorator):
13
13
  tolerations: list[Toleration]
14
14
  volume_mounts: list[VolumeMount]
15
15
  sidecars: list[SidecarContainer]
16
+ affinity: dict
16
17
 
17
18
  def __init__(self, *args, cpu='1', memory='1Gi', gpu=None, image=None,
18
19
  node_selector=None, tolerations: list[Toleration] = None,
19
20
  volume_mounts: list[VolumeMount] = None, pool: str = None,
20
- sidecars: list[SidecarContainer] = None, **kwargs):
21
+ sidecars: list[SidecarContainer] = None, affinity: dict = None,
22
+ **kwargs):
21
23
  super().__init__(*args, **kwargs)
22
24
  self.set_resource(cpu, memory, gpu)
23
25
  self.image = image
@@ -26,6 +28,7 @@ class TaskDecorator(BaseDecorator):
26
28
  self.volume_mounts = volume_mounts or []
27
29
  self.pool = pool
28
30
  self.sidecars = sidecars or []
31
+ self.affinity = affinity or {}
29
32
  self._is_flowhub_task = True
30
33
 
31
34
  def set_resource(self, cpu, memory, gpu):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeflowhub
3
- Version: 0.2.4
3
+ Version: 0.4.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='0.2.4',
5
+ version='0.4.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