::: IT인터넷 :::

Jenkins 파이썬 빌드 구성의 예제 코드 만들기 (1)

곰탱이푸우 2021. 12. 23. 08:20
이전 포스팅에서 Jenkins의 파이썬 빌드 구성을 위해, Docker로 빌드 에이전트로 만들고 ssh로 Jenkins에 연결하는 방법을 다뤘다.
다음 포스팅을 참고한다.
또한 Docker 기반의 파이썬 빌드 에이전트를 이용하여 Jenkins에서 파이썬 빌드를 구성하는 방법도 다뤘다.
다음 포스팅을 참고한다.
그러나 파이썬 빌드에 사용할 예제 코드에 대한 언급이 없었다.
파이썬 빌드를 위한 예제 코드를 생성하는 방법에 대해 정리한다.
기능을 정의하고 코드와 테스트를 구현하고, whl (Wheel) 패키지로 빌드하여 배포하는 과정을 다룬다.
  1. 기능 정의
  2. 프로젝트 생성
  3. 기능 코드 작성 (srtest)
  4. 테스트 코드 작성 (tests)
  5. 패키지 정의
  6. 테스트
  7. 형상 관리
 
이번 포스팅은 1. 기능 정의 부터 3. 기능 정의 부분까지 다루고, 이후 과정은 다음 포스팅에서 다룬다.
 

기능 정의

파이썬 빌드를 위한 예제 코드이므로 복잡한 기능을 정의하지는 않는다.
그러나 Hello World와 같이 너무 간단한 코드를 작성하면, 빌드 테스트에 적합하지 않다.
 
따라서 외부 라이브러리를 가져다 쓰면서 최대한 단순한 패키지를 작성하는데 중점을 두었다.
예제 코드는 init과 display라는 2개의 간단한 옵션을 가지고 있다.
이메일 전송 관련 설정 정보를 읽어와서 화면에 출력하는 패키지로, 실제로 이메일을 보내는 기능은 구현하지 않았다.
테스트를 위해 최대한 간단하게 구성했다.
 

init 기능

해당 패키지의 설정 파일을 생성하는 기능을 수행한다.
실제로는 패키지 내에 포함 된 설정 파일을 현재 경로로 복사하는 기능을 수행한다.
 
현재 경로 하위에 config 폴더를 생성하고, config 폴더에 파이썬 패키지에 포함 된 yml 파일을 복사한다.
 

display 기능

복사한 yml 파일 내부의 값을 읽어와서 화면에 출력하는 기능이다.
날짜 값은 현재 날짜에 맞게 변경해서 출력한다.
 

프로젝트 생성

위의 기능에 맞춰 코드 작성을 진행한다.
먼저 프로젝트 폴더를 구성하고, 각 기능별로 코드를 작성한다.
 

프로젝트 폴더 구성

파이썬 프로젝트 폴더 구성은 이전 포스팅에서 다룬 적이 있다.
 
다음 포스팅을 참고한다.
작성하는 패키지의 전체 구성은 다음과 같다.
수행할 기능을 정의한 srtest 폴더와, 정의한 기능을 테스트하는 tests 폴더로 구성된다.
나머지 setup.py, requirements.txt, test_requirements.txt는 파이썬 패키지 생성을 위한 파일들이다.
root
├─ .gitingnore    # git의 commit 제외 파일
├─ README.md    # git의 프로젝트 첫페이지 안내문
├─ setup.py    # 빌드, 배포를 위한 설정
├─ srtest    # 해당 프로그램이 수행 기능을 정의한 소스코드 폴더
│    ├─ __init__.py    # 해당 경로가 파이썬 패키지에 포함됨을 알리는 역할
│    ├─ emailclient.py    # 해당 프로그램의 부가 기능을 정의한 소스코드
│    ├─ info.py    # 해당 패키지의 이름과 버전 정보 (setup.py에서 사용)
│    ├─ main.py    # 해당 프로그램의 전체 기능을 정의한 소스코드
│    └─ srtest-config    # 프로그램 실행에 필요한 파일 포함
│        ├─ __init__.py    # 해당 경로가 파이썬 패키지에 포함됨을 알리는 역할
│        └─ config.yml    # 해당 프로그램에서 사용하는 설정 파일
├─ requirements.txt    # 설치할때 필요한 라이브러리
├─ tests    # 소스코드에 정의 된 기능들에 대한 단위(유닛) 테스트를 정의한 폴더
│    ├─ __init__.py    # 해당 경로가 파이썬 패키지에 포함됨을 알리는 역할
│    ├─ test_emailclient.py    # emailclient.py에 정의한 기능의 단위(유닛) 테스트
│    └─ srtest-config    # 테스트 수행에 필요한 파일 포함
│        └─ config.yml    # 테스트 수행하는데 필요한 설정 파일
└─ test_requirements.txt    # 테스트할때 필요한 라이브러리

 

.gitignore, README.md, setup.py에 대한 설명은 생략한다.
 
관련 내용은 다음 포스팅을 참고한다.

info.py

패키지의 이름과 버전을 정의한다.
패키지 작성할 때 가장 먼저 수정해야 하는 값이다.
__package_name__ = 'srtest-python'
__version__ = '1.0.0'
 
개발 중인 버전이므로 버전 뒤에 dev를 붙이는 것이 좋다.
예를 들어 최초 작성 버전이 1.0.0이면 1.0.0.dev와 같은 형식이다.
 
테스트를 위한 예제이므로 dev 없이 진행한다.
 

기능 코드 작성 (srtest)

해당 패키지가 수행할 기능을 정의한다.
 

main.py

setup.py 내부의 entry_points 항목은 패키지를 실행할때 가장 먼저 실행 될 부분을 가리킨다.
 
아래 코드를 보면 srtest.main:main으로 되어 있다.
srtest 경로 하위의 main.py 파일에 정의 된 main() 함수를 호출하는 것이다.
... 생략 ...

entry_points={
        "console_scripts": ["srtest-python=srtest.main:main"]
    },

... 생략 ...
 
main() 함수의 전체 코드는 다음과 같다. (더보기를 클릭한다.)
더보기

main.py

import argparse
import os
import shutil
from .emailclient import *


def main():
    """
    pip install 을 통해 srtest-python을 설치하고 나면, 다음 과정을 수행한다.
    - init - config.xml 파일을 지정 경로에 복사한다.
    - display - config.xml 파일을 읽고 설정 값을 출력한다.
    """

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="mode")

    subparsers.add_parser("init")
    subparsers.add_parser("display")

    args = parser.parse_args()
    print(f"-- args --\r\n{args.mode}\r\n")

    config_folder = "srtest-config"
    config_file = "config.yml"

    src_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), config_folder, config_file)
    dst_file = os.path.join(os.getcwd(), config_folder, config_file)

    print(f"-- src_file --\r\n{src_file}\r\n")
    print(f"-- dst_file --\r\n{dst_file}\r\n")

    if args.mode == "init":
        dst_folder = os.path.dirname(dst_file)

        # config 파일을 복사할 경로 확인 후, 해당 경로에 설정 파일을 복사한다.
        if os.path.exists(dst_folder) is False:
            os.mkdir(dst_folder)
            shutil.copyfile(src_file, dst_file)
            print(f"Copy the config.xml is completed!\r\n")
        else:
            raise FileExistsError(f"config directory already exists: {dst_folder}!")
    elif args.mode == "display" or args.mode is None:
        if os.path.exists(dst_file):
            client = EmailClient()

            client.set_curr_time()
            result = client.set_email_info(dst_file)

            print(f"-- result --")

            for key in result.keys():
                element = result[key]
                print(f"{key}: {element}")
        else:
            raise FileNotFoundError(f"config file is not exist!:\r\n{dst_file}")
    else:
        parser.parse_args(['-h'])


if __name__ == "__main__":
    main()

 

 
main() 함수 호출
가장 먼저 코드 최하단을 살펴보면 다음과 같다.
if __name__ == "__main__":
    main()
 
터미널에서 해당 패키지를 실행하면 파이썬에서는 __name__에 __main__을 전달한다.
__main__이 전달되면 main() 함수를 호출한다는 의미이다.
 
import 부분
import는 해당 py 파일 내부에서 사용하는 라이브러리들을 정의한다.
 
main.py 에서는 다음과 같이 정의했다. 자세한 설명은 주석을 참고한다.
import argparse    # 실행 명령에 함께 전달한 인자 파싱 (파이썬 기본 제공)
import os    # 파이썬의 파일과 경로 처리 (파이썬 기본 제공)
import shutil    # 파이썬의 파일과 경로 처리 (파이썬 기본 제공)
from .emailclient import *    # 이메일 관련 설정 파일 처리와 화면 출력 (직접 작성 필요)
 
main() 함수 정의
main 함수 정의 부분이다. """은 함수의 Doc String 관련 부분으로, 함수에 대한 기능과 전달 인자에 대해 설명한다.
 
자세한 설명은 주석을 참고한다.
def main():
    """
    pip install 을 통해 srtest-python을 설치하고 나면, 다음 과정을 수행한다.
    - init - config.xml 파일을 지정 경로에 복사한다.
    - display - config.xml 파일을 읽고 설정 값을 출력한다.
    """
 
전달 받은 인자 파싱
전달 받은 인자를 파싱하는 부분이다. import에 정의한 argparse를 사용한다.
전달 받은 인자를 args.mode에 저장하고 화면에 출력한다.
 
자세한 내용은 주석을 참고한다.
    parser = argparse.ArgumentParser()    # argparse 라이브러리의 ArgumentParser 클래스 객체 생성
    subparsers = parser.add_subparsers(dest="mode")    # subparser 추가 (파싱 결과는 mode에 저장)
    
    # 전달 받은 값이 init 또는 display면 mode에 저장하도록 설정
    # 두 값이 아닌 다른 값이 전달 되면 mode에 해당 값 저장
    subparsers.add_parser("init")    # parser 추가 (init)
    subparsers.add_parser("display")    # parser 추가 (display)
    
    args = parser.parse_args()    # 실제 전달 받은 인자 파싱
    print(f"-- args --\r\n{args.mode}\r\n")    # 전달 받은 인자 출력
 
 
설정 파일 경로 설정
미리 정의한 설정 파일의 경로와 복사할 경로를 지정한다.
예제 패키지이므로 직접 입력하지 않고 기본 값을 설정하여 사용한다.
 
자세한 내용은 주석을 참고한다.
    config_folder = "srtest-config"    # 설정 파일이 저장 된 폴더명 정의
    config_file = "config.yml"    # 설정 파일명 지정
    
    # 패키지 내부의 설정 파일 경로 생성 (절대 경로)
    src_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), \
              config_folder, config_file)   
              
    # 설정 파일을 복사할 경로 생성 (현재경로\config_folder\config_file)
    # 즉, 현재경로\srtest-config\config.yml 형태의 경로 (절대 경로)
    dst_file = os.path.join(os.getcwd(), config_folder, config_file)   
    
    # 생성한 경로 출력
    print(f"-- src_file --\r\n{src_file}\r\n")
    print(f"-- dst_file --\r\n{dst_file}\r\n")
 
init 기능 정의
실행할 때 전달한 인자가 init인 경우에 대한 기능을 정의한다.
mode에 저장 된 값이 init인 경우를 조건문으로 지정했다.
 
자세한 내용은 주석을 참고한다.
    if args.mode == "init":
        # dirname 함수로 dst_file에서 파일명을 제거한다. 폴더 경로만 남는다.
        dst_folder = os.path.dirname(dst_file)
        
        # config 파일을 복사할 경로 확인 후, 해당 경로에 설정 파일을 복사한다.
        if os.path.exists(dst_folder) is False:    # 폴더가 없으면 폴더 생성
            os.mkdir(dst_folder)
            shutil.copyfile(src_file, dst_file)    # 파일 복사
            print(f"Copy the config.xml is completed!\r\n")    # 로그 출력
            
        else:
            # 이미 파일이 존재하면 예외 발생
            raise FileExistsError(f"config directory already exists: {dst_folder}!")
 
display 기능 정의
실행할 때 전달한 인자가 display인 경우에 대한 기능을 정의한다.
mode에 저장 된 값이 display인 경우를 조건문의 elif (else if)로 지정했다.
또한 전달한 인자가 없으면 display로 인식하도록 조건문을 추가했다.
 
자세한 내용은 주석을 참고한다.
    elif args.mode == "display" or args.mode is None:
        # init 실행 이후 설정 파일이 존재하는 경우
        if os.path.exists(dst_file):
            # emailclient.py 내부의 EmailClient 클래스의 객체 생성
            client = EmailClient()
            
            # 해당 객체의 set_curr_time 함수 호출하여 현재 시간 설정
            client.set_curr_time()
            
            # set_email_info 함수에 설정 파일 경로를 인자로 전달하여 호출
            # 해당 함수는 설정 파일의 값을 읽고, 날짜 부분을 현재 날짜로 변경하여 출력
            result = client.set_email_info(dst_file)
            
            # 읽어 온 결과 출력
            print(f"-- result --")
            
            # Key-Value 형태의 Dictionary 타입이므로 for문으로 하나씩 출력
            for key in result.keys():
                element = result[key]
                print(f"{key}: {element}")
                
        # 설정 파일이 없으면 예외 발생
        else:
            raise FileNotFoundError(f"config file is not exist!:\r\n{dst_file}")
 
전달 된 인자가 init, display, 공백이 아닌 경우 예외 처리
전달 된 인자가 init, display 또는 미지정이 아닌 엉뚱한 값인 경우 도움말을 출력한다.
 
자세한 내용은 주석을 참고한다.
    else:
        # argparse 객체의 parse_args 함수에 ['-h'] 옵션을 전달하면 도움말 출력
        parser.parse_args(['-h'])
 
 

srtest-config\config.yml

기능 구현 부분을 보기 전에 먼저 yml 타입의 설정 파일을 볼 필요가 있다.
해당 설정 파일은 다음과 같은 yaml 포맷을 가진다.
 
아래 date 부분의 값을 변경해서 화면에 출력한다.
 
자세한 내용은 주석을 참고한다.
common:    # 최상위 key
  email:    # 상위 key
    # :를 구분자로 좌측이 key, 오른쪽이 value
    sender: "no-reply@bearpooh.com"    # 원하는 값으로 수정
    receiver: "이메일계정@gmail.com"    # 원하는 값으로 수정
    subject: "test-mail"
    date: -@year-@month-@day    # 이 부분의 값을 변경한다

 

emailclient.py

main 함수의 display 기능에서 emailclient.py에 정의한 클래스를 객체로 선언하여 사용했다.
emailclient.py는 해당 클래스의 기능을 구체적으로 정의한다.
 
전체 코드는 다음과 같다. (더보기를 클릭한다.)
더보기
더보기
더보기

emailclient.py

from datetime import datetime
import textwrap
import yaml


class EmailClient:
    """
    EmailClient 는 config.yml 파일을 읽어서 메시지 형식을 채우는 클래스이다.
    """
    def __init__(self):
        self.curr_time = None

    def set_curr_time(self):
        """
        현재 시간을 기록한다.
        """
        self.curr_time = datetime.now()

    def set_email_info(self, config_file_path: str) -> dict:
        """
        YAML 파일을 읽어서 메시지 형식을 채운다.

        :param config_file_path: yaml 파일의 경로
        """       
        with open(config_file_path, "r", encoding="utf-8") as yaml_file:
            yaml_dict = yaml.load(yaml_file, Loader=yaml.Loader)

        # yaml 파일을 dict 형태로 읽어서, 원하는 값을 추출한다.
        # 추출한 값을 intermediate_payload 라는 dictionary 에 key-value 로 저장한다.
        intermediate_payload = yaml_dict['common']['email']

        curr_time = self.curr_time
        year = str(curr_time.year)
        month = str(curr_time.month).zfill(2)
        day = str(curr_time.day).zfill(2)

        print(f"-- Current Date --")
        print(f"year = {year}")
        print(f"month={month}")
        print(f"day={day}\r\n")

        # 입력으로 받은 값으로 치환한다.
        payload = self._apply_value_to_magic_keyword(dictionary=intermediate_payload,
                                                    year=year, month=month, day=day)

        message_body = textwrap.dedent(f"""
            {payload["date"]}에 테스트를 수행했습니다.

            From bearpooh.com auto-mailing
            """)

        message = dict()

        message['From'] = payload["sender"]
        message['To'] = payload["receiver"]
        message["Subject"] = payload["subject"]
        message["Body"] = message_body

        return message

    @staticmethod
    def _apply_value_to_magic_keyword(dictionary, year, month, day):
        """
        YAML 파일의 date 부분을 입력으로 받은 year, month, day로 치환한다.

        :param dictionary: YAML 파일에서 읽은 key-value 형태의 값
        :param year: @year를 치환할 연도
        :param month: @month를 치환할 월
        :param day: @day를 치환할 일
        """
        for key in dictionary.keys():
            element = dictionary[key]

            if "-@year" in element:
                res = element.replace("-@year", year)
                dictionary[key] = res
                element = res
            if "@month" in element:
                res = element.replace("@month", month)
                dictionary[key] = res
                element = res
            if "@day" in element:
                res = element.replace("@day", day)
                dictionary[key] = res

        return dictionary
 
 
import 부분
import는 해당 py 파일 내부에서 사용하는 라이브러리들을 정의한다.
from datetime import datetime    # 날짜 처리 관련 라이브러리 (파이썬 기본 제공)
import textwrap    # 여러 행의 문자열을 처리하는 라이브러리 (파이썬 기본 제공)
import yaml    # yaml 포맷 처리 라이브러리 (설치 필요)

 

yaml 라이브러리는 pip로 설치가 필요한 라이브러리이다.
이 부분이 중요하다. 해당 라이브러리 사용을 위해 이후에 다룰 requirement.txt에서 해당 라이브러리와 버전을 지정한다.
패키지를 설치하기에 앞서 requirement.txt에 정의한 라이브러리를 먼저 설치한다.
 
클래스 정의 부분
클래스 정의 부분이다. """은 함수의 Doc String 관련 부분으로, 클래스에 대한 기능에 대해 설명한다.

 

자세한 설명은 주석을 참고한다.
class EmailClient:
    """
    EmailClient 는 config.yml 파일을 읽어서 메시지 형식을 채우는 클래스이다.
    """
 
__init__ 함수
파이썬 클래스의 생성자 역할을 수행한다. 기본 전달 인자로 self를 받는다.
해당 인자에 대한 Doc String 작성은 하지 않아도 된다.
클래스 내부에서 사용할 변수를 선언하고 초기값을 할당한다.
 
자세한 설명은 주석을 참고한다.
    def __init__(self):
        # 전달 받은 self는 클래스 객체 자체를 의미한다.
        # self.변수명으로 변수를 선언하고, = None으로 초기 값을 할당한다.
        self.curr_time = None
 
set_curr_time 함수
시스템의 현재 날짜와 시간을 저장하는 함수이다.
 
자세한 설명은 주석을 참고한다.
    def set_curr_time(self):
        """
        현재 시간을 기록한다.
        """
        # datetime 라이브러리의 now() 함수를 호출하여 시스템의 현재 날짜와 시간 획득
        # 생성자에서 선언한 curr_time 속성에 저장
        self.curr_time = datetime.now()
 
 
set_email_info 함수
YAML 포맷의 이메일 관련 설정 파일을 읽어서 Dictionary 타입으로 반환하는  함수이다.
 
자세한 설명은 주석을 참고한다.
    def set_email_info(self, config_file_path: str) -> dict:
        """
        YAML 파일을 읽어서 메시지 형식을 채운다.
        :param config_file_path: yaml 파일의 경로
        """       
        
        # 전달 받은 설정 파일 경로의 파일을 읽고 yaml 포맷을 파싱한다.
        # yaml 라이브러리는 설치가 필요하다
        with open(config_file_path, "r", encoding="utf-8") as yaml_file:
            yaml_dict = yaml.load(yaml_file, Loader=yaml.Loader)
            
        # yaml 파일을 dict 형태로 읽어서, 원하는 값을 추출한다.
        # 추출한 값을 intermediate_payload 라는 dictionary 에 key-value 로 저장한다.
        intermediate_payload = yaml_dict['common']['email']
        
        # 클래스 생성자가 확보한 현재 시간을 가져와서 연, 월, 일 정보를 추출한다
        # zfill은 앞에 0을 채운다. 1인 경우 01
        curr_time = self.curr_time
        year = str(curr_time.year)
        month = str(curr_time.month).zfill(2)   
        day = str(curr_time.day).zfill(2)
       
        # 값을 출력한다
        print(f"-- Current Date --")
        print(f"year = {year}")
        print(f"month={month}")
        print(f"day={day}\r\n")
        
        # _apply_value_to_magic_keyword 함수에 전달하여 연, 월, 일 정보를 변경한다.
        payload = self._apply_value_to_magic_keyword(dictionary=intermediate_payload,
                                                    year=year, month=month, day=day)
                                                    
        # 메시지 본문을 생성한다. textwrap을 사용하여 여러 줄의 문자열을 한번에 처리한다.
        # 변경한 값을 반환 받은 payload["date"]의 값을 사용한다.
        message_body = textwrap.dedent(f"""
            {payload["date"]}에 테스트를 수행했습니다.
            From bearpooh.com auto-mailing
            """)
            
        # 이메일 전송에 필요한 정보들을 dictionary 타입으로 채운다
        message = dict()
        message['From'] = payload["sender"]    # 발신자
        message['To'] = payload["receiver"]    # 수신자
        message["Subject"] = payload["subject"]    # 제목
        message["Body"] = message_body    # textwrap으로 생성한 본문
        
        # 최종 결과 반환
        return message
 
_apply_value_to_magic_keyword 함수
앞서 다룬 set_email_info에서 본문의 연, 월, 일을 변경하기 위해 호출한 함수이다.
본문의 연, 월, 일 부분을 전달 받은 값으로 변경하고, dictionary 형태로 반환한다.
 
해당 함수는 클래스 내부에서만 사용하는 pretected 메소드라서, 함수명 앞에 관용적인 표현인 언더바(_) 한 개를 추가했다.
파이썬은 외부에서도 호출 가능해서 큰 의미는 없지만 코드 이해의 가독성과 직관성을 위해 추가하는 편이다.
 
자세한 설명은 주석을 참고한다.
    # 전달 받은 인자만 사용하고, 클래스 또는 인스턴스 변수는 사용하지 않으므로
    # staticmethod 데코레이터 사용
    @staticmethod
    def _apply_value_to_magic_keyword(dictionary, year, month, day):
        """
        YAML 파일의 date 부분을 입력으로 받은 year, month, day로 치환한다.
        :param dictionary: YAML 파일에서 읽은 key-value 형태의 값
        :param year: @year를 치환할 연도
        :param month: @month를 치환할 월
        :param day: @day를 치환할 일
        """
        
        # 전달 받은 key-value 형태의 dictionary에서 반복문으로 key 탐색
        for key in dictionary.keys():
            # key에 해당하는 value를 element에 저장
            element = dictionary[key]
            
            # value가 -@year, @month, @day인 경우 값 변경
            # str 타입이기 때문에 내장 함수인 replace 사용
            if "-@year" in element:
                res = element.replace("-@year", year)
                dictionary[key] = res
                element = res

            if "@month" in element:
                res = element.replace("@month", month)
                dictionary[key] = res
                element = res

            if "@day" in element:
                res = element.replace("@day", day)
                dictionary[key] = res
                
        # 변경한 dictionary 반환
        return dictionary

 

이후 과정인 4. 테스트 코드 작성 (tests), 5. 패키지 정의, 6. 테스트, 7. 형상 관리는 다음 포스팅을 참고한다.

 

Jenkins 파이썬 빌드 구성의 예제 코드 만들기 (2)

파이썬 빌드를 위한 예제 코드를 생성하는 방법에 대해 정리한다. 기능을 정의하고 코드와 테스트를 구현하고, whl (Wheel) 패키지로 빌드하여 배포하는 과정을 다룬다. 기능 정의 프로젝트 생성

www.bearpooh.com