xython 4.5.1__tar.gz → 4.5.2__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 (29) hide show
  1. {xython-4.5.1 → xython-4.5.2}/MANIFEST.in +1 -0
  2. {xython-4.5.1/src/xython.egg-info → xython-4.5.2}/PKG-INFO +3 -2
  3. {xython-4.5.1 → xython-4.5.2}/pyproject.toml +3 -3
  4. {xython-4.5.1 → xython-4.5.2}/src/xython/__init__.py +1 -1
  5. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_chrome.py +2 -1
  6. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_color.py +10 -12
  7. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_common.py +1 -0
  8. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_edge.py +98 -46
  9. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_excel.py +30 -11
  10. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_outlook.py +4 -1
  11. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_re.py +4 -3
  12. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_time.py +5 -5
  13. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_util.py +1 -0
  14. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_word.py +5 -5
  15. {xython-4.5.1 → xython-4.5.2/src/xython.egg-info}/PKG-INFO +3 -2
  16. {xython-4.5.1 → xython-4.5.2}/README.md +0 -0
  17. {xython-4.5.1 → xython-4.5.2}/requirements.txt +0 -0
  18. {xython-4.5.1 → xython-4.5.2}/setup.cfg +0 -0
  19. {xython-4.5.1 → xython-4.5.2}/src/xython/_easy_start.py +0 -0
  20. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_auto.py +0 -0
  21. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_db.py +0 -0
  22. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_excel_event.py +0 -0
  23. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_hwp.py +0 -0
  24. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_list.py +0 -0
  25. {xython-4.5.1 → xython-4.5.2}/src/xython/xy_map.py +0 -0
  26. {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/SOURCES.txt +0 -0
  27. {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/dependency_links.txt +0 -0
  28. {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/requires.txt +0 -0
  29. {xython-4.5.1 → xython-4.5.2}/src/xython.egg-info/top_level.txt +0 -0
@@ -22,3 +22,4 @@ include src/xython/xy_re.py
22
22
  include src/xython/xy_time.py
23
23
  include src/xython/xy_util.py
24
24
  include src/xython/xy_word.py
25
+ include src/xython/_easy_start.py
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xython
3
- Version: 4.5.1
3
+ Version: 4.5.2
4
4
  Summary: xython package
5
5
  Author-email: "SJ.Park" <sjpkorea@naver.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://www.xython.co.kr
7
+ Project-URL: Homepage, https://blog.naver.com/xython
8
+ Project-URL: Documentation, https://sjpkorea.github.io/xython.github.io/
8
9
  Requires-Python: >=3.8
9
10
  Description-Content-Type: text/markdown
10
11
  Requires-Dist: korean_lunar_calendar
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xython"
7
- version = "4.5.1"
7
+ version = "4.5.2"
8
8
  description = "xython package"
9
9
  readme = {file = "README.md", content-type = "text/markdown"}
10
10
  requires-python = ">=3.8"
@@ -26,8 +26,8 @@ dependencies = [
26
26
  ]
27
27
 
28
28
  [project.urls]
29
- Homepage = "https://www.xython.co.kr"
30
-
29
+ Homepage = "https://blog.naver.com/xython"
30
+ Documentation = "https://sjpkorea.github.io/xython.github.io/"
31
31
  [tool.setuptools.packages.find]
32
32
  where = ["src"]
33
33
 
@@ -1,7 +1,7 @@
1
1
  # __init__.py
2
2
  # Copyright (C) 2026 (sjpkorea@naver.com) and contributors
3
3
 
4
-
4
+ #https://blog.naver.com/xython
5
5
  import inspect
6
6
  import os
7
7
  import sys
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import copy
2
3
  from html.parser import HTMLParser
3
4
  from xython import xy_util, xy_excel, xy_re
@@ -1123,7 +1124,7 @@ class xy_chrome:
1123
1124
  print(f"[테이블 추출] URL: {url}")
1124
1125
  print(f" → 총 {len(tables_3d)}개 테이블 발견")
1125
1126
  for i, table in enumerate(tables_3d):
1126
- print(f" 테이블[{i}]: {len(table)}행 × {max(len(r) for r in table) if table else 0}열")
1127
+ print(f" 테이블[{i}]: {len(table)}행 x {max(len(r) for r in table) if table else 0}열")
1127
1128
 
1128
1129
  return tables_3d
1129
1130
 
@@ -1,7 +1,12 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import re, math # 내장모듈
2
3
  import win32api, win32gui
3
4
  from xython import xy_re, xy_common # xython 모듈
4
5
 
6
+ from unittest.mock import patch
7
+ with patch("ctypes.windll.user32.SetProcessDPIAware", autospec=True):
8
+ import pyautogui
9
+
5
10
 
6
11
  class xy_color:
7
12
  """
@@ -140,7 +145,7 @@ class xy_color:
140
145
  hsl값을 미세조정하는 부분
141
146
 
142
147
  pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
143
- 숫자일 때: 50 기준으로 ±변화 (30이면 -20, 70이면 +20)
148
+ 숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
144
149
  """
145
150
  if type(pm100) == type(123):
146
151
  l_value = pm100 - 50 # 50 기준 차이 (음수 가능 → 어둡게도 동작)
@@ -162,7 +167,7 @@ class xy_color:
162
167
  hsl값을 미세조정하는 부분
163
168
 
164
169
  pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
165
- 숫자일 때: 50 기준으로 ±변화 (30이면 -20, 70이면 +20)
170
+ 숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
166
171
  """
167
172
  if type(pm100) == type(123):
168
173
  l_value = pm100 - 50 # 50 기준 차이 (음수 가능 → 어둡게도 동작)
@@ -996,7 +1001,7 @@ class xy_color:
996
1001
 
997
1002
  def get_analogous_colors(self, input_xcolor, angle=30, count=2):
998
1003
  """
999
- 유사색 생성 (색상환에서 ±angle도 이내의 색)
1004
+ 유사색 생성 (색상환에서 +-angle도 이내의 색)
1000
1005
  count=2이면 양쪽 1개씩, count=4이면 양쪽 2개씩
1001
1006
  반환: [[r,g,b], ...]
1002
1007
  """
@@ -1291,7 +1296,7 @@ class xy_color:
1291
1296
 
1292
1297
  def get_split_complementary_colors(self, input_xcolor, angle=150):
1293
1298
  """
1294
- 분리 보색 배색 (보색에서 ±30도 위치의 두 색)
1299
+ 분리 보색 배색 (보색에서 +-30도 위치의 두 색)
1295
1300
  반환: [원색rgb, 분리보색1rgb, 분리보색2rgb]
1296
1301
  """
1297
1302
  hsl = self.to_hsl(input_xcolor)
@@ -1616,7 +1621,7 @@ class xy_color:
1616
1621
  def hsl_to_rgb_by_pm100(self, input_hsl, pm100):
1617
1622
  """
1618
1623
  pm100 : ++, --, 70등의 값이 들어오면 변화를 시켜주는 것
1619
- 숫자일 때: 50 기준으로 ±변화 (30이면 -20, 70이면 +20)
1624
+ 숫자일 때: 50 기준으로 +-변화 (30이면 -20, 70이면 +20)
1620
1625
 
1621
1626
  :param input_hsl: [h,s,l]값
1622
1627
  :param pm100:
@@ -1994,8 +1999,6 @@ class xy_color:
1994
1999
  result = '#{:02x}{:02x}{:02x}'.format(r, g, b)
1995
2000
  return result
1996
2001
 
1997
- def rgb_to_hex(self, input_rgb, option="#"):
1998
- return self.rgb_to_hex(input_rgb, option)
1999
2002
 
2000
2003
  def rgb_to_hex_rgb(self, r, g, b):
2001
2004
  """
@@ -2279,11 +2282,6 @@ class xy_color:
2279
2282
  result = self.rgb_to_close_56color_no(rgb_value)
2280
2283
  return result
2281
2284
 
2282
- def to_hex(self, input_xcolor):
2283
- """
2284
- 사용의 편의성을 위해 만듦
2285
- """
2286
- return self.to_hex(input_xcolor)
2287
2285
 
2288
2286
  def to_hex(self, input_xcolor):
2289
2287
  """
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import datetime
2
3
 
3
4
  class xy_common:
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import os, time, winreg, socket,re
2
3
  from html.parser import HTMLParser
3
4
  from DrissionPage import ChromiumPage, ChromiumOptions
@@ -168,9 +169,9 @@ class xy_edge:
168
169
  result.append(p)
169
170
 
170
171
  if result:
171
- print(f" Edge 경로 발견: {result[0]}")
172
+ print(f" Edge 경로 발견: {result[0]}")
172
173
  else:
173
- print(" Edge 경로를 자동으로 찾지 못했습니다.")
174
+ print(" Edge 경로를 자동으로 찾지 못했습니다.")
174
175
 
175
176
  return result
176
177
 
@@ -306,10 +307,10 @@ class xy_edge:
306
307
  """
307
308
  input_value 에 따라 탭을 선택하고, is_active=True 이면 활성화합니다.
308
309
 
309
- None / "" / "pass"현재 상태 유지
310
- "visible" / "active" / "activate"화면에 보이는 탭
311
- int해당 번호 탭 (1-based)
312
- str제목 부분 일치 탭
310
+ None / "" / "pass" 현재 상태 유지
311
+ "visible" / "active" / "activate" 화면에 보이는 탭
312
+ int 해당 번호 탭 (1-based)
313
+ str 제목 부분 일치 탭
313
314
  """
314
315
  if input_value in (None, "", "pass"):
315
316
  pass
@@ -526,7 +527,7 @@ class xy_edge:
526
527
  def clear_and_input(self, selector, text):
527
528
  """
528
529
  기존 값을 지우고 새 값을 입력합니다.
529
- Ctrl+ADelete 방식으로 확실하게 지웁니다.
530
+ Ctrl+A Delete 방식으로 확실하게 지웁니다.
530
531
  """
531
532
  try:
532
533
  element = self.page.ele(selector, timeout=5)
@@ -568,7 +569,7 @@ class xy_edge:
568
569
  def click_by_full_css_path(self, css_path, timeout = 5.0, scroll_to = True):
569
570
  """
570
571
  CSS 전체 경로(Full CSS Selector)로 요소를 찾아 클릭합니다.
571
- DevToolsCopyCopy selector 결과를 그대로 사용할 수 있습니다.
572
+ DevTools Copy Copy selector 결과를 그대로 사용할 수 있습니다.
572
573
  """
573
574
  try:
574
575
  element = self._active.ele(f'css:{css_path}', timeout=timeout)
@@ -582,7 +583,7 @@ class xy_edge:
582
583
  print(f"[click_by_full_css_path] 클릭 성공: {css_path}")
583
584
  return True
584
585
  except Exception as e:
585
- print(f"[click_by_full_css_path] 오류: {css_path}{e}")
586
+ print(f"[click_by_full_css_path] 오류: {css_path} {e}")
586
587
  return False
587
588
 
588
589
  def click_by_full_path(self, full_xpath):
@@ -666,7 +667,7 @@ class xy_edge:
666
667
  time.sleep(wait_time)
667
668
  return True
668
669
 
669
- print(f" '{target_text}' 클릭 실패")
670
+ print(f"X '{target_text}' 클릭 실패")
670
671
  return False
671
672
 
672
673
  except Exception as e:
@@ -716,7 +717,7 @@ class xy_edge:
716
717
  print(f"[iframe_click] 요소 없음: {locator}")
717
718
  return False
718
719
  el.click()
719
- print(f" iframe 클릭 완료: {locator}")
720
+ print(f" iframe 클릭 완료: {locator}")
720
721
  return True
721
722
  except Exception as e:
722
723
  print(f"[iframe_click] 오류: {e}")
@@ -735,7 +736,7 @@ class xy_edge:
735
736
  time.sleep(wait_time)
736
737
 
737
738
  if len(self.page.tabs) > initial_tab_count:
738
- print(" 새 탭 생성됨")
739
+ print(" 새 탭 생성됨")
739
740
  self.select_page_by_latest_page()
740
741
  time.sleep(0.5)
741
742
 
@@ -835,7 +836,7 @@ class xy_edge:
835
836
  if self.page and (self.new_browser_opened or force):
836
837
  try:
837
838
  self.page.quit()
838
- print(" 브라우저를 종료했습니다.")
839
+ print(" 브라우저를 종료했습니다.")
839
840
  except Exception:
840
841
  pass
841
842
 
@@ -939,7 +940,7 @@ class xy_edge:
939
940
  closed += 1
940
941
  except Exception:
941
942
  pass
942
- print(f" {closed}개 탭/팝업 닫음 (유지: {current_url})")
943
+ print(f" {closed}개 탭/팝업 닫음 (유지: {current_url})")
943
944
  return closed
944
945
  except Exception as e:
945
946
  print(f"[close_other_tabs] 오류: {e}")
@@ -1020,13 +1021,12 @@ class xy_edge:
1020
1021
  if (btn && isVisible(btn)) return btn;
1021
1022
  }
1022
1023
 
1023
- // 우선순위 3: 텍스트가 닫기/X/× 인 버튼
1024
+ // 우선순위 3: 텍스트가 닫기 인 버튼
1024
1025
  var allBtns = root.querySelectorAll('button, a, span, div, td');
1025
1026
  for (var btn of allBtns) {
1026
1027
  if (!isVisible(btn)) continue;
1027
1028
  var txt = (btn.innerText || btn.textContent || '').trim();
1028
- if (txt === '닫기' || txt === 'X' || txt === 'x' ||
1029
- txt === '×' || txt === '✕' || txt === '✖' || txt === 'Close') {
1029
+ if (txt === '닫기' || txt === 'X' || txt === 'x' || txt === 'Close') {
1030
1030
  return btn;
1031
1031
  }
1032
1032
  }
@@ -1115,7 +1115,7 @@ class xy_edge:
1115
1115
  closed_count = page.run_js(dhx_close_js)
1116
1116
  total_closed = closed_count or 0
1117
1117
 
1118
- # 재시도: 0.5초 간격 × 최대 5회 (팝업이 순차적으로 뜨는 경우 대비)
1118
+ # 재시도: 0.5초 간격 x 최대 5회 (팝업이 순차적으로 뜨는 경우 대비)
1119
1119
  for retry in range(5):
1120
1120
  time.sleep(0.5)
1121
1121
 
@@ -1565,19 +1565,19 @@ class xy_edge:
1565
1565
  # 1단계: 기존 디버그 브라우저 확인
1566
1566
  if self._is_port_open(port):
1567
1567
  try:
1568
- print(f" 포트 {port}에서 실행 중인 디버그 브라우저 발견! 연결 시도...")
1568
+ print(f" 포트 {port}에서 실행 중인 디버그 브라우저 발견! 연결 시도...")
1569
1569
  self.page = ChromiumPage(addr_or_opts=f'127.0.0.1:{port}')
1570
1570
  self.browser = self.page.browser
1571
- print(" 기존 브라우저 연결 성공!\n")
1571
+ print(" 기존 브라우저 연결 성공!\n")
1572
1572
  if self.url:
1573
- print(f" URL로 이동: {self.url}")
1573
+ print(f" URL로 이동: {self.url}")
1574
1574
  self.page.get(self.url)
1575
1575
  return
1576
1576
  except Exception as e:
1577
- print(f" 연결 실패: {e}\n 새 브라우저를 실행합니다...\n")
1577
+ print(f"X 연결 실패: {e}\n 새 브라우저를 실행합니다...\n")
1578
1578
 
1579
1579
  # 2단계: 새 Edge 실행
1580
- print(f" 포트 {port}에 실행 중인 브라우저가 없습니다. 새 Edge를 실행합니다...\n")
1580
+ print(f" 포트 {port}에 실행 중인 브라우저가 없습니다. 새 Edge를 실행합니다...\n")
1581
1581
 
1582
1582
  co = ChromiumOptions()
1583
1583
  co.set_local_port(port)
@@ -1585,10 +1585,10 @@ class xy_edge:
1585
1585
 
1586
1586
  # Edge 경로 설정 — 중복 호출 제거
1587
1587
  if self.edge_paths:
1588
- print(f" Edge 경로 설정: {self.edge_paths[0]}")
1588
+ print(f" Edge 경로 설정: {self.edge_paths[0]}")
1589
1589
  co.set_browser_path(self.edge_paths[0])
1590
1590
  else:
1591
- print(" Edge 경로를 찾지 못해 시스템 기본 브라우저를 사용합니다.")
1591
+ print(" Edge 경로를 찾지 못해 시스템 기본 브라우저를 사용합니다.")
1592
1592
 
1593
1593
  co.set_argument('--test-type')
1594
1594
  co.set_argument('--start-maximized')
@@ -1605,12 +1605,12 @@ class xy_edge:
1605
1605
  self.page = ChromiumPage(co)
1606
1606
  self.browser = self.page.browser
1607
1607
  self.new_browser_opened = True
1608
- print(" 새 Edge 브라우저 실행 성공!\n")
1608
+ print(" 새 Edge 브라우저 실행 성공!\n")
1609
1609
  if self.url:
1610
- print(f" URL로 이동: {self.url}")
1610
+ print(f" URL로 이동: {self.url}")
1611
1611
  self.page.get(self.url)
1612
1612
  except Exception as e:
1613
- print(f" Edge 실행 실패: {e}")
1613
+ print(f"X Edge 실행 실패: {e}")
1614
1614
  raise
1615
1615
 
1616
1616
  def count_iframe(self):
@@ -1654,7 +1654,7 @@ class xy_edge:
1654
1654
 
1655
1655
  def drag_and_drop(self, source_selector, target_selector, timeout = 5.0):
1656
1656
  """
1657
- 요소요소 드래그 앤 드롭 (드롭존이 file input 이 아닌 경우).
1657
+ 요소 요소 드래그 앤 드롭 (드롭존이 file input 이 아닌 경우).
1658
1658
  파일 드롭은 drop_file_to_element() 를 사용하세요.
1659
1659
  """
1660
1660
  try:
@@ -1861,8 +1861,8 @@ class xy_edge:
1861
1861
  print(
1862
1862
  f" [{idx}] <{info['tag']}> '{info['text'][:40]}' "
1863
1863
  f"| 중심=({center_x},{center_y}) "
1864
- f"| {rect['width']}×{rect['height']} "
1865
- f"| {'' if is_visible else ''}"
1864
+ f"| {rect['width']}x{rect['height']} "
1865
+ f"| {'' if is_visible else 'X'}"
1866
1866
  )
1867
1867
  except Exception as e:
1868
1868
  print(f" [{idx}] 정보 수집 오류: {e}")
@@ -1955,7 +1955,7 @@ class xy_edge:
1955
1955
 
1956
1956
  # 모든 항목이 포함된 경우
1957
1957
  if not missing:
1958
- print(f" 완전 매칭!iframe_id: '{iframe_id}'")
1958
+ print(f" 완전 매칭! iframe_id: '{iframe_id}'")
1959
1959
  matched_ids.append(iframe_id)
1960
1960
 
1961
1961
  except Exception as e:
@@ -2216,10 +2216,10 @@ class xy_edge:
2216
2216
  }})()
2217
2217
  """
2218
2218
  tables_3d = self.page.run_js(js) or []
2219
- print(f"[테이블 추출] URL: {url} 총 {len(tables_3d)}개 테이블")
2219
+ print(f"[테이블 추출] URL: {url} -> 총 {len(tables_3d)}개 테이블")
2220
2220
  for i, table in enumerate(tables_3d):
2221
2221
  cols = max(len(r) for r in table) if table else 0
2222
- print(f" 테이블[{i}]: {len(table)}행 × {cols}열")
2222
+ print(f" 테이블[{i}]: {len(table)}행 x {cols}열")
2223
2223
  return tables_3d
2224
2224
 
2225
2225
  def get_all_text_by_iframe_id(self, iframe_id):
@@ -2378,7 +2378,7 @@ class xy_edge:
2378
2378
  }
2379
2379
  print(
2380
2380
  f"[캐럿 위치] 태그={info['element_tag']} 인덱스={info['caret_index']} "
2381
- f"스크린({sx},{sy})모니터{monitor} 로컬({local_x},{sy})"
2381
+ f"스크린({sx},{sy}) 모니터{monitor} 로컬({local_x},{sy})"
2382
2382
  )
2383
2383
  return info
2384
2384
 
@@ -2443,9 +2443,9 @@ class xy_edge:
2443
2443
 
2444
2444
  Args:
2445
2445
  path: [grand_idx, papa_idx, son_idx, ...] 형태의 정수 리스트.
2446
- 예) [0]최상위 첫 번째 iframe
2447
- [0, 1]최상위 첫 번째 iframe 안의 두 번째 자식 iframe
2448
- [0, 1, 0]그 아래 첫 번째 손자 iframe
2446
+ 예) [0] 최상위 첫 번째 iframe
2447
+ [0, 1] 최상위 첫 번째 iframe 안의 두 번째 자식 iframe
2448
+ [0, 1, 0] 그 아래 첫 번째 손자 iframe
2449
2449
 
2450
2450
  Returns:
2451
2451
  frame 객체 또는 None (경로가 잘못된 경우)
@@ -2529,7 +2529,7 @@ class xy_edge:
2529
2529
  local_x = x if monitor == 1 else x - monitor_width
2530
2530
 
2531
2531
  info = {"x": x, "y": y, "monitor": monitor, "local_x": local_x, "local_y": y}
2532
- print(f"[마우스 위치] 절대({x}, {y})모니터 {monitor}번 로컬({local_x}, {y})")
2532
+ print(f"[마우스 위치] 절대({x}, {y}) 모니터 {monitor}번 로컬({local_x}, {y})")
2533
2533
  return info
2534
2534
 
2535
2535
  def get_page_data_by_xpath_v2(self, xpath, tab_name=None):
@@ -2805,7 +2805,7 @@ class xy_edge:
2805
2805
  button = self.page.ele(selector, timeout=timeout)
2806
2806
  if button:
2807
2807
  button.click()
2808
- print(" '무시하고 보내기' 버튼 클릭 완료")
2808
+ print(" '무시하고 보내기' 버튼 클릭 완료")
2809
2809
  time.sleep(0.5)
2810
2810
  return True
2811
2811
  except Exception:
@@ -2895,7 +2895,7 @@ class xy_edge:
2895
2895
  cursor_mark = " ★★★ [클릭가능]" if el.get('cursor') in ['pointer', 'hand'] else ""
2896
2896
  close_mark = " ★ [CLOSE관련]" if any(
2897
2897
  k in (el.get('className', '') + el.get('text', '')).lower()
2898
- for k in ['close', '닫기', 'x', '×', 'btn', 'button']
2898
+ for k in ['close', '닫기', 'X', 'x', 'btn', 'button']
2899
2899
  ) else ""
2900
2900
 
2901
2901
  print(f" [{el.get('rootClass', '')}]")
@@ -3179,7 +3179,7 @@ class xy_edge:
3179
3179
  """
3180
3180
  try:
3181
3181
  frame_obj.get_screenshot(path=save_path)
3182
- print(f" iframe 스크린샷 저장: {save_path}")
3182
+ print(f" iframe 스크린샷 저장: {save_path}")
3183
3183
  return True
3184
3184
  except Exception as e:
3185
3185
  print(f"[iframe_screenshot] 오류: {e}")
@@ -3208,7 +3208,7 @@ class xy_edge:
3208
3208
  """
3209
3209
  result = self.page.run_js(js)
3210
3210
  time.sleep(0.4)
3211
- print(f"[scroll_to_make_visible_by_coord] 뷰포트({viewport_x}, {viewport_y}){result}px")
3211
+ print(f"[scroll_to_make_visible_by_coord] 뷰포트({viewport_x}, {viewport_y}) {result}px")
3212
3212
  return True
3213
3213
  except Exception as e:
3214
3214
  print(f"[scroll_to_make_visible_by_coord] 오류: {e}")
@@ -3286,7 +3286,7 @@ class xy_edge:
3286
3286
  print(f"select 요소를 찾을 수 없음 (id: {select_id})")
3287
3287
  return False
3288
3288
 
3289
- print(f" select 요소 발견!")
3289
+ print(f" select 요소 발견!")
3290
3290
 
3291
3291
  options = select_element.eles('tag:option')
3292
3292
  print(f"옵션 개수: {len(options)}")
@@ -3403,7 +3403,7 @@ class xy_edge:
3403
3403
  if opt.attr("value") == option_value:
3404
3404
  opt.click()
3405
3405
  break
3406
- print(f" iframe select 완료: {locator}")
3406
+ print(f" iframe select 완료: {locator}")
3407
3407
  return True
3408
3408
  except Exception as e:
3409
3409
  print(f"[iframe_select_option] 오류: {e}")
@@ -3687,10 +3687,62 @@ class xy_edge:
3687
3687
  if clear_first:
3688
3688
  el.clear()
3689
3689
  el.input(value)
3690
- print(f" iframe 입력 완료: {locator} ← '{value}'")
3690
+ print(f" iframe 입력 완료: {locator} ← '{value}'")
3691
3691
  return True
3692
3692
  except Exception as e:
3693
3693
  print(f"[iframe_write] 오류: {e}")
3694
3694
  return False
3695
3695
 
3696
3696
 
3697
+ def downLoad_attachments_for_1st(self):
3698
+ #회사의 첨부화일을 다운 받는 함수
3699
+ attach_area = self.page.ele('#AttFileInfo')
3700
+ if not attach_area:
3701
+ print("첨부파일 없음")
3702
+ return
3703
+ inks = attach_area.eles('tag:a')
3704
+ print(f'총 {len(links)}개 파일 발견\n')
3705
+
3706
+ for link in enumerate(Links):
3707
+ print(f"[{i+1}/{len(links)}] {link.text.strip()}클릭")
3708
+
3709
+ try:
3710
+ Link.click()
3711
+ except Exception as e:
3712
+ print(f'?? 클릭 오류: {e}')
3713
+ continue
3714
+ time.sleep(0.5) #클릭 간 짧은 간격만 줌
3715
+
3716
+ print('*클릭 완료! 브라우저가 알아서 다운로드 중...')
3717
+
3718
+ def save_page_as_pdf(self, filename=None, folder=None):
3719
+ # 현재 연결된 페이지를 PDF로
3720
+ if not filename.endswith(".pdf"): filename +=".pdf"
3721
+
3722
+ if not folder:
3723
+ download_folder = Path.home() / "Downloads"
3724
+ else:
3725
+ download_folder = folder
3726
+
3727
+ save_path = os.path.join(download_folder, filename)
3728
+ pdf_params = {
3729
+ "printBackground": True,
3730
+ "paperWidth": 8.27, # A4 L₩ (inch) - 210mm
3731
+ "aperHeight": 11.69, # A4 ₩0l (inch) - 297mm
3732
+ "marginTop": 0.5,
3733
+ "marginBottom":0.5,
3734
+ "marginleft": 0.5,
3735
+ "marginRight": 0.5,
3736
+ "displayHeaderFooter": False, # 혜더/푸터 술길
3737
+ "scale": 1.0,
3738
+ }
3739
+
3740
+ # PDF 생성 및 지장
3741
+ pdf_data = self.page.run_cdp("Page.printToPDF", **pdf_params)
3742
+ # Base64 디코딩 후 파일 제품
3743
+
3744
+ pdf_bytes = base64.b64decode(pdf_data["data"])
3745
+ with open(save_path, "wb") as f:
3746
+ f.write(pdf_bytes)
3747
+ print(f"PDF 저장 완료: (save_path)")
3748
+ return save_path
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import re, math, string, random, os, itertools, copy, time
2
3
  import pywintypes, webbrowser, psutil
3
4
  import ctypes
@@ -118,12 +119,16 @@ class xy_excel:
118
119
  def add_text_at_left(self, sheet_name='', xyxy='', input_value=None):
119
120
  sheet, [x1, y1, x2, y2], rng = self.check_sheet_n_xyxy(sheet_name, xyxy)
120
121
  for x, y in itertools.product(range(x1, x2 + 1), range(y1, y2 + 1)):
121
- sheet.Cells(x, y).Value = str(input_value) + str(sheet.Cells(x, y).Value)
122
+ current_value = sheet.Cells(x, y).Value
123
+ if current_value == None: current_value = ""
124
+ sheet.Cells(x, y).Value = str(input_value) + str(current_value)
122
125
 
123
126
  def add_text_at_right(self, sheet_name='', xyxy='', input_value=None):
124
127
  sheet, [x1, y1, x2, y2], rng = self.check_sheet_n_xyxy(sheet_name, xyxy)
125
128
  for x, y in itertools.product(range(x1, x2 + 1), range(y1, y2 + 1)):
126
- sheet.Cells(x, y).Value = str(sheet.Cells(x, y).Value) + str(input_value)
129
+ current_value = sheet.Cells(x, y).Value
130
+ if current_value == None: current_value = ""
131
+ sheet.Cells(x, y).Value = str(current_value) + str(input_value)
127
132
 
128
133
  def all_sheet_name(self):
129
134
  """get_all_sheet_name 별칭 (하위 호환)."""
@@ -10671,7 +10676,7 @@ class xy_excel:
10671
10676
  sheet.Cells(write_x, write_y).Value = write_x, write_y
10672
10677
 
10673
10678
  def wait_ready(self, timeout_sec=30, interval=0.2):
10674
- """계산·화면 갱신 대기."""
10679
+ """계산 : 화면 갱신 대기"""
10675
10680
  end = time.time() + timeout_sec
10676
10681
  while time.time() < end:
10677
10682
  try:
@@ -11380,32 +11385,32 @@ class xy_excel:
11380
11385
  if data_type == "l1d":
11381
11386
  data = self.varx["data"][input_value]
11382
11387
  self.new_sheet()
11383
- self.write("", [1, 1], data)
11388
+ self.write("", [1, 1], data, True)
11384
11389
  elif data_type == "l2d":
11385
11390
  data = self.varx["data"][input_value]
11386
11391
  self.new_sheet()
11387
- self.write("", [1, 1], data)
11392
+ self.write("", [1, 1], data, True)
11388
11393
  elif data_type == "t2d":
11389
11394
  data = self.varx["data"][input_value]
11390
11395
  changed_data = data.splitlines()
11391
11396
  self.new_sheet()
11392
- self.write("", [1, 1], changed_data)
11397
+ self.write("", [1, 1], changed_data, True)
11393
11398
  elif data_type == "table2d":
11394
11399
  data = self.varx["data"][input_value]
11395
11400
  self.new_sheet()
11396
- self.write("", [1, 1], data)
11401
+ self.write("", [1, 1], data, True)
11397
11402
  elif data_type == "d1d":
11398
11403
  data = self.varx["data"][input_value]
11399
11404
  self.new_sheet()
11400
11405
  for index, key in enumerate(data.keys()):
11401
- self.write("", [1 + index, 1], key)
11402
- self.write("", [1 + index, 2], str(data[key]))
11406
+ self.write("", [1 + index, 1], key, True)
11407
+ self.write("", [1 + index, 2], str(data[key]), True)
11403
11408
  elif data_type == "d2d":
11404
11409
  data = self.varx["data"][input_value]
11405
11410
  self.new_sheet()
11406
11411
  for index, key in enumerate(data.keys()):
11407
- self.write("", [1 + index, 1], key)
11408
- self.write("", [1 + index, 2], str(data[key]))
11412
+ self.write("", [1 + index, 1], key, True)
11413
+ self.write("", [1 + index, 2], str(data[key]), True)
11409
11414
 
11410
11415
  def write_title_from_first_line(self, sheet_name='', xyxy=''):
11411
11416
  """
@@ -11527,4 +11532,18 @@ class xy_excel:
11527
11532
  merged_wb.Close()
11528
11533
 
11529
11534
 
11535
+ def all_data_for_quote_no(self, gijun_no):
11536
+ # 엑셀의 관리시트에서 관리 번호를 입력하면 그에대한 정보를 갖고오는것
11537
+ data_start_x= 2
11538
+ xy = self.get_address_for_bottom_end("", [3,5])
11539
+ if xy[0]-50 < data_start_x:
11540
+ xy[0] =50+ data_start_x
11541
+ l2d_15 = self.read_range("", [xy[0]-50, 1, xy[0], 40])
11542
+
11543
+ #맨앞부분에 엑셀의 가로본호를 넣는다 (엑셀의 위치를 알고싶어서)
11544
+ for index, item in enumerate(l2d_15):
11545
+ item.append(index+xy[0]-50)
11546
+ # 읽어온 15개의 자료중에 기준번호와 같은것만 필터링하는 것
11547
+ l2d_filtered = self.xyutil.filter_12d_for_same_with_yline_and_value(l2d_15, 3, gijun_no)
11548
+ return l2d_filtered
11530
11549
 
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import os, datetime, csv # 내장모듈
2
3
  import win32com.client # pywin32의 모듈
3
4
  from collections import Counter
@@ -23,6 +24,8 @@ class xy_outlook:
23
24
  'Importance', 'Sensitivity', 'Categories', 'Attachments',
24
25
  'ConversationTopic', 'Size', 'UnRead', 'EntryID', ] # Importance 중복 제거, 유용한 속성 추가
25
26
 
27
+
28
+ def start(self):
26
29
  self.outlook_program = win32com.client.dynamic.Dispatch('Outlook.Application')
27
30
  self.outlook = self.outlook_program.GetNamespace("MAPI")
28
31
 
@@ -687,7 +690,7 @@ class xy_outlook:
687
690
 
688
691
  def get_attachment_info_for_mail(self, one_mail):
689
692
  """
690
- 메일 첨부파일의 이름·크기를 딕셔너리 리스트로 반환
693
+ 메일 첨부파일의 이름/크기를 딕셔너리 리스트로 반환
691
694
  [{"name": "file.xlsx", "size": 12345}, ...]
692
695
  """
693
696
  result = []
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import re
2
3
  from pathlib import Path
3
4
 
@@ -567,7 +568,7 @@ class xy_re:
567
568
  return bool(re.match(r"^[0-9.]+$", input_text))
568
569
 
569
570
  def is_resident_id(self, input_text):
570
- """주민등록번호 형식(13자리, 날짜·성별 자릿수)."""
571
+ """주민등록번호 형식(13자리, 날짜/성별 자릿수)."""
571
572
  text = str(input_text).replace("-", "").strip()
572
573
  return bool(
573
574
  re.fullmatch(r"\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{7}", text)
@@ -604,7 +605,7 @@ class xy_re:
604
605
  return self.search_with_resql(resql, input_text)
605
606
 
606
607
  def normalize_whitespace(self, input_text):
607
- """연속 공백·빈줄 정리."""
608
+ """연속 공백/빈줄 정리."""
608
609
  text = re.sub(r"[ \t]+", " ", input_text)
609
610
  return self.delete_over_2_empty_lines(text).strip()
610
611
 
@@ -896,7 +897,7 @@ class xy_re:
896
897
  return self.search_with_resql(self.change_xsql_to_resql(input_xsql), input_text)
897
898
 
898
899
  def setup_begin_end(self, begin=True, end=True):
899
- """전체 일치용 [시작]/[끝] 접두·접미 설정."""
900
+ """전체 일치용 [시작]/[끝] 접두/접미 설정"""
900
901
  self.setup["begin"] = begin
901
902
  self.setup["end"] = end
902
903
 
@@ -7,8 +7,8 @@ from datetime import datetime, timezone, timedelta, date
7
7
 
8
8
  from xython import xy_re, xy_common # xython 모듈
9
9
  """
10
- 공휴일 = 법정 공휴일로 지정된 날 (신정, 설날, 추석 등). 그날이 토·일요일과 겹쳐도 여전히 공휴일입니다.
11
- 주말 = 토·일요일 자체. 법정 공휴일이 아닌 일반 토·일요일은 "주말"이지 "공휴일"이 아닙니다.
10
+ 공휴일 = 법정 공휴일로 지정된 날 (신정, 설날, 추석 등). 그날이 토/일요일과 겹쳐도 여전히 공휴일입니다.
11
+ 주말 = 토/일요일 자체. 법정 공휴일이 아닌 일반 토/일요일은 "주말"이지 "공휴일"이 아닙니다.
12
12
  겹치는 경우 = 예를 들어 현충일(6/6)이 토요일이면 → 공휴일이면서 동시에 주말입니다.
13
13
  """
14
14
 
@@ -1764,7 +1764,7 @@ class xy_time():
1764
1764
  def check_temp_holiday(self, base_date: date, rule: str, already: set):
1765
1765
  """
1766
1766
  대체공휴일 계산.
1767
- rule: "" → 없음 / "일" → 일요일 겹침 / "토일" → 토·일요일 겹침
1767
+ rule: "" → 없음 / "일" → 일요일 겹침 / "토일" → 토/일요일 겹침
1768
1768
  """
1769
1769
  if not rule:
1770
1770
  return None
@@ -2209,7 +2209,7 @@ class xy_time():
2209
2209
 
2210
2210
  분류 기준:
2211
2211
  - holiday : 법정 공휴일 (주말과 겹쳐도 공휴일 우선)
2212
- - weekend : 공휴일이 아닌 순수 토·일요일
2212
+ - weekend : 공휴일이 아닌 순수 토/일요일
2213
2213
  - workday : 근무일(평일 + 비공휴일)
2214
2214
 
2215
2215
  Parameters
@@ -2713,7 +2713,7 @@ class xy_time():
2713
2713
 
2714
2714
  def get_holidays_in_range_include_weekend(self, start, end) -> list:
2715
2715
  """
2716
- start ~ end 사이의 공휴일 + 주말(토·일)을 날짜 순서대로 반환합니다.
2716
+ start ~ end 사이의 공휴일 + 주말(토/일)을 날짜 순서대로 반환합니다.
2717
2717
  (공휴일과 주말을 모두 포함, 근무일만 제외)
2718
2718
 
2719
2719
  Parameters
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  import csv, difflib, bisect, filecmp, random, copy, subprocess
2
3
  import shutil, pickle, inspect, string, math, time
3
4
  import re, os, collections, zipfile, sys, pyperclip, pywintypes
@@ -6434,7 +6434,7 @@ class xy_word:
6434
6434
 
6435
6435
 
6436
6436
  def normalize(self, text: str) -> str:
6437
- """공백·줄바꿈·탭 등을 모두 제거한 순수 문자열 반환"""
6437
+ """공백/줄바꿈/탭 등을 모두 제거한 순수 문자열 반환"""
6438
6438
  return "".join(text.split())
6439
6439
 
6440
6440
 
@@ -6490,7 +6490,7 @@ class xy_word:
6490
6490
  total_sections = len(section_ranges)
6491
6491
  print(f"[1단계] 총 {total_sections}개 섹션 발견")
6492
6492
 
6493
- # 제목도 normalize() 적용하여 공백·줄바꿈 제거 후 그룹화
6493
+ # 제목도 normalize() 적용하여 공백/줄바꿈 제거 후 그룹화
6494
6494
  title_sections = defaultdict(list)
6495
6495
  for idx, section in enumerate(section_ranges):
6496
6496
  normalized_title = self.normalize(section["prev_title"])
@@ -6508,7 +6508,7 @@ class xy_word:
6508
6508
  if dup_title_count > 0:
6509
6509
  print("\n [중복 제목 목록]")
6510
6510
  for title, indices in duplicates_to_check.items():
6511
- print(f" · '{title}' — {len(indices)}개 발견")
6511
+ print(f" '{title}' — {len(indices)}개 발견")
6512
6512
  print()
6513
6513
 
6514
6514
  ranges_to_delete = []
@@ -6520,7 +6520,7 @@ class xy_word:
6520
6520
  section = section_ranges[idx]
6521
6521
  content_range = self.doc.Range(section['start'], section['end'])
6522
6522
 
6523
- # 내용도 normalize() 적용 — 공백·줄바꿈 완전 제거 후 순수 값만 비교
6523
+ # 내용도 normalize() 적용 — 공백/줄바꿈 완전 제거 후 순수 값만 비교
6524
6524
  content = self.normalize(content_range.Text)
6525
6525
 
6526
6526
  if content in content_map:
@@ -6535,7 +6535,7 @@ class xy_word:
6535
6535
  if actual_dup_count > 0:
6536
6536
  print("\n [삭제 예정 항목]")
6537
6537
  for title, count in deleted_titles.items():
6538
- print(f" · '{title}' — {count}개 삭제")
6538
+ print(f" '{title}' — {count}개 삭제")
6539
6539
  print()
6540
6540
 
6541
6541
  # 역순 삭제
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xython
3
- Version: 4.5.1
3
+ Version: 4.5.2
4
4
  Summary: xython package
5
5
  Author-email: "SJ.Park" <sjpkorea@naver.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://www.xython.co.kr
7
+ Project-URL: Homepage, https://blog.naver.com/xython
8
+ Project-URL: Documentation, https://sjpkorea.github.io/xython.github.io/
8
9
  Requires-Python: >=3.8
9
10
  Description-Content-Type: text/markdown
10
11
  Requires-Dist: korean_lunar_calendar
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes