python 파일을 실행하기 위해서는 관련 패키지를 설치해야 합니다.

다른 컴퓨터로 옮겨서 실행하거나 배포 시에는 매우 번거로운 작업입니다.

그래서 exe 파일로 만들면 이러한 작업 없이 손쉽게 실행할 수 있습니다.

1) PyInstaller 설치

PyInstaller 는 다음 명령으로 손쉽게 설치가 가능합니다.

pip install pyinstaller

 

2) exe 파일 생성

pyinstaller -w -F jbdesk.py

 

pyinstaller -w -F jbdesk.py 명령은 pyinstaller를 사용하여 Python 스크립트 (jbdesk.py)를 실행 파일로 변환하는 명령입니다. 각 옵션의 의미는 다음과 같습니다:

  1. -w (또는 --windowed):
    • 이 옵션은 GUI 애플리케이션을 만들 때 사용됩니다. 콘솔 창이 나타나지 않도록 합니다. 기본적으로 PyInstaller는 GUI 애플리케이션을 실행할 때 콘솔 창을 열지만, -w 옵션을 사용하면 콘솔 창 없이 애플리케이션이 실행됩니다.
    • 주로 GUI 애플리케이션(예: PyQt5, Tkinter)에서 사용됩니다.
  2. -F (또는 --onefile):
    • 이 옵션은 모든 파일을 단일 실행 파일로 패키징합니다. 즉, 여러 개의 파일이나 디렉토리로 나누지 않고 하나의 실행 파일(.exe)로 만들어 배포할 수 있습니다.
    • 이 옵션을 사용하지 않으면 여러 파일로 분리된 실행 파일이 생성됩니다.

따라서, pyinstaller -w -F jbdesk.py 명령은 jbdesk.py 파일을 콘솔 창 없이 실행되는 단일 실행 파일로 패키징합니다.

Posted by 제이브레인
,

앱을 사용하다가 tray icon 으로 축소를 하면 좀더 편하겠다는 생각이 들었습니다.

1) 코드 생성

사용한 ChatGPT 프롬프트는 다음과 같습니다.

pyqt5 로 Hello 라는 버튼을 표시해주세요.
축소 아이콘을 클릭하면 누르면 tray 로 축소하고
종료 아이콘을 클릭하면 앱을 종료하시겠습니까? 라고 묻고 Yes 선택 시 앱을 종료하고 다른 것을 선택하면 tray 로 축소하도록 해주세요.

 

생성된 코드는 다음과 같으며 일부 버그는 수정하였습니다.

아래 코드는 다음과 같이 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch1.4

 

tray_icon.py

import sys

from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMenu, QAction, QMessageBox, QSystemTrayIcon


class HelloApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setWindowTitle("PyQt5 Tray Example")
        self.setGeometry(100, 100, 300, 200)

        # 버튼 생성
        self.button = QPushButton("Hello", self)
        self.button.setGeometry(100, 80, 100, 40)
        self.button.clicked.connect(self.show_message)

        # 시스템 트레이 아이콘 생성
        self.tray_icon = QSystemTrayIcon(self)
        icon_path = "tray_icon.png"  # 아이콘 경로 확인 (아이콘 파일이 있어야 합니다)
        self.tray_icon.setIcon(QIcon(icon_path))
        self.tray_icon.setToolTip("PyQt5 Tray App")
        self.tray_icon.show()  # 트레이 아이콘 표시

        # 트레이 메뉴 설정
        tray_menu = QMenu()
        restore_action = QAction("열기", self)
        restore_action.triggered.connect(self.show_window)

        quit_action = QAction("종료", self)
        quit_action.triggered.connect(self.exit_app)

        tray_menu.addAction(restore_action)
        tray_menu.addAction(quit_action)

        self.tray_icon.setContextMenu(tray_menu)
        self.tray_icon.activated.connect(self.tray_icon_clicked)

    def show_message(self):
        QMessageBox.information(self, "Message", "Hello, PyQt5!")

    def closeEvent(self, event):
        """창 닫기 버튼(X) 클릭 시 앱 종료 여부 확인"""
        reply = QMessageBox.question(self, "종료 확인", "앱을 종료하시겠습니까?",
                                     QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

        if reply == QMessageBox.Yes:
            self.exit_app()
        else:
            event.ignore()  # 창을 최소화하여 숨깁니다
            self.hide()  # 창 숨기기
            self.tray_icon.showMessage(
                "앱이 실행 중", "트레이에서 실행 중입니다.", QSystemTrayIcon.Information, 2000
            )

    def show_window(self):
        """트레이 메뉴에서 '열기' 선택 시 창 복원"""
        self.showNormal()
        self.activateWindow()

    def tray_icon_clicked(self, reason):
        """트레이 아이콘 클릭 시 창을 다시 보이게 함"""
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            self.show_window()

    def exit_app(self):
        """트레이 메뉴에서 '종료' 선택 시 앱 완전히 종료"""
        self.tray_icon.hide()  # 트레이 아이콘 숨기기
        QApplication.quit()  # PyQt5 이벤트 루프 종료
        sys.exit(0)  # 강제 종료


if __name__ == "__main__":
    app = QApplication(sys.argv)

    # 모든 창이 닫혀도 앱이 종료되지 않도록 설정
    app.setQuitOnLastWindowClosed(False)

    # 아이콘 설정
    app.setWindowIcon(QIcon("tray_icon.png"))

    window = HelloApp()
    window.show()
    sys.exit(app.exec_())

 

2) 코드 실행

코드 실행 후 tray 축소 전입니다.

 

축소 아이콘을 클릭하면 아래와 같이 tray icon 으로 표시됩니다.

 

3) JbDesk 에 코드 통합

tray_icon.py 에서 tray 관련 부분만 jbesk.py 에 머지하면 됩니다.

코드는 다음 위치에 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cat jbDeskExample/jbDesk/ch1.4/jbesk.py

다음은 코드 통합 후 실행한 모습이며 종료 아이콘 클릭 시 앱을 종료할지 물어봅니다.

Yes 클릭 시 앱이 종료되며 No 선택 시 Tray icon 으로 축소됩니다.

Posted by 제이브레인
,

timezone 이 다른 로그를 분석할 때는 시간 변환이 필요합니다.

예를 들면 서버가 timezone 이 미국으로 설정된 서버에서 로그를 보고 해당 서버의 한국 시간을 변환하려면 복잡한 시차 적용이 필요해서 어떤 시간에 해당 로그가 발생했는지 쉽게 확인이 힘듭니다.

이럴 경우 timezone 변환을 자동으로 해주는 기능이 있다면 매우 편리할 것입니다.

1) 코드 생성

ChatGPT 프롬프트로 다음을 사용하였습니다.

log 의 경우 time 이 포함된 스트링은 여러가지 pattern 이 존재할 수 있습니다.

예를 들면 '%Y-%m-%d %H:%M:%S %Z', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M' 등의 여러 형태가 존재할 수 있습니다. 이 경우 함수에 date format 이 어떤 것인지 하나하나 입력하기 보다는 자동으로 인식하면 편리할 것입니다.

그래서 아래와 같이 자동으로 인식하도록 한 것입니다.

python 으로 아래와 같이 time 을 표시하는 문자열이 존재하는 경우 timezone 을 변환하는 함수를 만들어주세요.
함수에는 log_text, source_timezone, dest_timezone 파라미터를 입력 받습니다.
log_text 는 아래 문자열과 같은 로그 text 를 입력받고
source_timezone, dest_timezone 은 US/Pacific, Asia/Seoul 과 같은 time zone 을 입력받습니다.
return 은 time zone 이 변환된  로그 text 를 리턴합니다.
log_text 중 time 이 포함된 문자열은 다음과 같은 문자열이 올 수 있습니다.
    DATE_PATTERNS = [
        (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [A-Z]{3}', '%Y-%m-%d %H:%M:%S %Z'),
        (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', '%Y-%m-%d %H:%M:%S'),
        (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}', '%Y-%m-%d %H:%M'),
        (r'\d{2}/\d{2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2} [APap][Mm]', '%m/%d/%Y %I:%M:%S %p'),
        (r'\d{2}/\d{2}/\d{4} \d{1,2}:\d{1,2} [APap][Mm]', '%m/%d/%Y %I:%M %p'),
        (r'\d{4}-\d{1,2}-\d{1,2} [APap][Mm] \d{1,2}:\d{1,2}:\d{1,2}', '%Y-%m-%d %p %I:%M:%S'),
        (r'\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} [APap][Mm]', '%Y-%m-%d %I:%M:%S %p'),
        (r'\d{2}/[A-Za-z]{3}/\d{4}:\d{2}:\d{2}:\d{2} [-+]\d{4}', '%d/%b/%Y:%H:%M:%S %z')
    ]

192.168.2.82 - - [27/Mar/2025:00:04:06 -0700] "GET /test HTTP/1.1" 200 - "-" "axios/0.21.4" 73ms "962fff801aa10f24" "a9a7a0fd8e5dd842" "962fff801aa10f24"

 

생성된 코드는 다음과 같습니다.

아래 코드는 다음과 같이 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch1.3

 

convert_log_timezone.py

import re
from datetime import datetime
import pytz

DATE_PATTERNS = [
    (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [A-Z]{3}', '%Y-%m-%d %H:%M:%S %Z'),
    (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', '%Y-%m-%d %H:%M:%S'),
    (r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}', '%Y-%m-%d %H:%M'),
    (r'\d{2}/\d{2}/\d{4} \d{1,2}:\d{1,2}:\d{1,2} [APap][Mm]', '%m/%d/%Y %I:%M:%S %p'),
    (r'\d{2}/\d{2}/\d{4} \d{1,2}:\d{1,2} [APap][Mm]', '%m/%d/%Y %I:%M %p'),
    (r'\d{4}-\d{1,2}-\d{1,2} [APap][Mm] \d{1,2}:\d{1,2}:\d{1,2}', '%Y-%m-%d %p %I:%M:%S'),
    (r'\d{4}-\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2} [APap][Mm]', '%Y-%m-%d %I:%M:%S %p'),
    (r'\d{2}/[A-Za-z]{3}/\d{4}:\d{2}:\d{2}:\d{2} [-+]\d{4}', '%d/%b/%Y:%H:%M:%S %z')
]

def convert_log_timezone(log_text: str, source_timezone: str, dest_timezone: str) -> str:
    for pattern, date_format in DATE_PATTERNS:
        match = re.search(pattern, log_text)
        if match:
            datetime_str = match.group()
            try:
                dt = datetime.strptime(datetime_str, date_format)
                if '%z' not in date_format and '%Z' not in date_format:
                    source_tz = pytz.timezone(source_timezone)
                    dt = source_tz.localize(dt)
                dt = dt.astimezone(pytz.timezone(dest_timezone))
                new_datetime_str = dt.strftime(date_format)
                log_text = log_text.replace(datetime_str, new_datetime_str)
            except ValueError:
                continue
    return log_text

# 예제 실행
log_text = '192.168.2.82 - - [27/Mar/2025:00:04:06 -0700] "GET /test HTTP/1.1" 200 - "-" "axios/0.21.4" 73ms "962fff801aa10f24" "a9a7a0fd8e5dd842" "962fff801aa10f24"'
converted_log = convert_log_timezone(log_text, 'US/Pacific', 'Asia/Seoul')
print(converted_log)

 

2) 코드 실행

위 코드를 실행한 결과입니다.

192.168.2.82 - - [27/Mar/2025:16:04:06 +0900] "GET /test HTTP/1.1" 200 - "-" "axios/0.21.4" 73ms "962fff801aa10f24" "a9a7a0fd8e5dd842" "962fff801aa10f24"

 

로그에서 변경된 부분은 다음과 같습니다.

미국 시간(US/Pacific) : 27/Mar/2025:00:04:06 -0700

한국 시간(Asia/Seoul) : 27/Mar/2025:16:04:06 +0900

US/Pacific 은 UTC-07:00 이고 Asia/Seoul 은 UTC+09:00 입니다.

한국 시간( Asia/Seoul )이 미국 시간(US/Pacific) 보다 16시간 빠릅니다.

변환이 정확하게 된 것을 확인 할 수 있습니다.

 

3) JbDesk 에 코드 통합

기존 "줄단위", "표기법" 메뉴에 "TimeZone"  메뉴를 추가하고 위 기능을 통합해보겠습니다.

사용한 ChatGPT 프롬프트는 다음과 같습니다.

다음 기능을 수행하는 python 코드를 pyqt5 를 사용하여 작성해주세요.
작성 후 에러도 테스트해주세요.

1. window 타이틀은 JbDesk 로 표시
2. window 메뉴에 "줄단위", "표기법"을 추가합니다.
"줄단위" 하위에는 "줄 공백 제거" 를 추가합니다.
"표기법" 하위에는 "camelCase", "snake_case", "PascalCase", "SCREAMING_SNAKE_CASE", "kebab-case", "Train-Case", "dot.notation" 를 추가합니다.
"TimeZone" 하위에 "로그 TimeZone 변환"을 추가합니다.

3. Window 내부에는 
첫째 라인에 선택한 Tool 기능 텍스트를 표시하고 
둘째 라인에는 "입력" 이라는 GroupBox 와 내부에 TextEdit 를 표시
세번째 라인에는 "변환"이라는 버튼을 표시합니다.
"변환" 버튼은 "변환"이라는 글자크기 만큼 버튼을 표시합니다.
다섯째 라인에는 "출력" 이라는 GroupBox 와 내부에 TextEdit 를 표시
4.
"줄 공백 제거"를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 줄 공백을 제거하여 "출력" text 에 표시합니다.
"camelCase" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 camelCase로 변환하여 "출력" text 에 표시합니다.
"snake_case" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 snake_case로 변환하여 "출력" text 에 표시합니다.
"PascalCase" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 PascalCase로 변환하여 "출력" text 에 표시합니다.
"SCREAMING_SNAKE_CASE" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 SCREAMING_SNAKE_CASE로 변환하여 "출력" text 에 표시합니다.
"Train-Case" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 Train-Case로 변환하여 "출력" text 에 표시합니다.
"dot.notation" 를 선택하고 "변환" 버튼을 누르면 "입력" 의 text 의 dot.notation로 변환하여 "출력" text 에 표시합니다.
5. 
"로그 TimeZone 변환"을 선택하면 
첫째 라인에 선택한 Tool 기능 텍스트를 표시하고 
둘째 라인에는 "입력" 이라는 GroupBox 와 내부에 TextEdit 를 표시
세번째 라인에는 "원본 TimeZone"이라는 Label에 "US/Pacific" , "Asia/Seoul" 선택하는 DropBox가 표시되고 우측에 "목표 TimeZone"이라는 Label에 "US/Pacific" , "Asia/Seoul" 선택하는 DropBox가 표시되고 우측에 "변환"이라는 버튼을 표시합니다.
"변환" 버튼은 "변환"이라는 글자크기 만큼 버튼을 표시합니다.
다섯째 라인에는 "출력" 이라는 GroupBox 와 내부에 TextEdit 를 표시하며
"변환" 버튼을 누르면 timezone 을 변환하여 "출력"에 표시합니다.

 

위 프롬프트가 생성한 코드에 기존 코드를 일부 병합하여 최종 코드를 생성하였습니다.

 

4) 통합 코드 실행

아래 이미지와 같이 입력된 text 의 timezone 이 변환됩니다.

Posted by 제이브레인
,