LoginSignup
100

個人的にお気に入りのPythonプロジェクトのファイル構成

Last updated at Posted at 2023-04-30

作りたい機能

  • PCやスマホから特定のデータベースにデータを蓄積する機能

    ※データの例としては収支や勉強時間

設計上の要望

  • AWSを使用してAPIとして使用したい
  • コマンドから手軽に使用したい
  • Docker上で動作させたい
  • 手軽にコードを更新できる仕組みとしたい

これが現在のファイル構成

設計上の要望を叶えようと作成したプロジェクトのファイル構成は以下の通りです

一部のファイル(折りたたみがあるファイル)に関しては記載例を載せています

  • github

    • workflows
      • deploy.yaml:Lambdaへのデプロイを自動化する
        name: AWS Lambda Deploy 
        on:
          push:
            branches:
              - [ここに指定したブランチにコードがプッシュされると、このワークフローが発火する]
        jobs:
          deploy:
            runs-on: ubuntu-latest
            permissions:
              id-token: write
              contents: read
        
            steps:
              - name: checkout
                uses: actions/checkout@v3
        
              - name: configure-aws-credentials
                uses: aws-actions/configure-aws-credentials@master
                with:
                  aws-region: ${{ secrets.AWS_REGION }}
                  role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
                  role-session-name: GitHubActions
        
              - name: get-caller-identity is allowed to run on role.
                run: aws sts get-caller-identity
        
              - name: setup-python
                uses: actions/setup-python@v3
                with:
                  python-version: '3.x'
        
              - name: lambda update
                run: |
                  pip3 install awscli
                  cd [デプロイしたいフォルダを指定] && zip -r package.zip ./*
                  aws lambda update-function-code --function-name [デプロイ先のLambda名を指定] --zip-file fileb://package.zip --publish
        
  • docker

    • docker-compose.yaml
      version: "3"
      services:
        コンテナ名:
          build:
            context: ../
            dockerfile: dockers/dockerfile
          env_file: ../.env # .envの内容がコンテナの環境変数として設定される
          environment:
            - PYTHONPATH=/var/app/src/core:/var/app/src/helper/util/python:/var/app/src/helper/lib/python
            - TZ=Asia/Tokyo
          volumes:
            - ../:/var/app
            - ~/.aws:/root/.aws
          tty: true
          restart: always
      
    • Dockerfile

    • docker-exec.sh:一発でbuildしてexecするシェルスクリプト
      docker compose up -d
      docker exec -it [コンテナ名] bash
      
  • src

    • core:ここをデプロイ時にLambdaに入れる

      • model:各リソースごとに作成

        • model1.py
      • service:機能ごとに作成する

        • service1.py
      • command.py:コマンドを実装
        # このような構成にすると、コマンド名をリソース名で定義した際に、APIと同じような使い方ができるようになる
        
        @click.group(invoke_without_command=True)
        @click.option('--get', '-g', 'method', flag_value='GET')
        @click.option('--post', '-p', 'method', flag_value='POST')
        @click.option('--update', '-u', 'method', flag_value='PATCH')
        @click.option('--delete', '-d', 'method', flag_value='DELETE')
        @click.pass_context
        def cli(ctx, method):
        	# コマンド名を取得する処理
            resource = ctx.info_name
        
            body = {}
            query_parameter = {}
            path_parameter = {}
        
            if method == const.GET_METHOD:
        				pass
            elif method == const.POST_METHOD:
                pass
            elif method == const.PATCH_METHOD:
                pass
            elif method == const.DELETE_METHOD:
                pass
                
            event = {
                'resource': f'/{resource}',
                'body': json.dumps(body),
                'queryStringParameters': query_parameter,
                'pathParameters': path_parameter,
                'httpMethod': method
            }
        		handler(event=event)
        
      • handler.py:各リクエストの入口
        def handler(event, context=None):
            resource = event.get('resource', '').replace('/', '')
        
            if resource == 'time':
        		# ここでインポートしないと循環インポートが起きてしまうことがある
                from service.time import Time
                return Time(event).main()
            elif resource == 'price':
                from service.price import Price
                return Price(event).main()
        	else:
                return {
                    "isBase64Encoded": False,
                    "statusCode": 404,
                    "headers": {},
        		}
        
    • helper:ここをデプロイ時にLambdaLayerに入れる

      • util
        • 外部連携

          • client.py
          • util.py
        • error.py:自作エラー

        • internal_api.py:内部の機能を使用する際に必要
          # 内部の別機能を使用する際はここのメソッドを使用しhandler経由で別機能を呼び出す
          
          def __create_event(method: str, resource: str = '', body: dict = {}, path_parameter: dict = {}, query_parameter: dict = {}):
              return {
                  'resource': f'/{resource}',
                  'body': json.dumps(body),
                  'queryStringParameters': query_parameter,
                  'pathParameters': path_parameter,
                  'httpMethod': method
              }
          
          def get(resource, query_parameter=None, path_parameter=None):
              event = __create_event(
                  method='GET',
                  resource=resource,
                  query_parameter=query_parameter,
                  path_parameter=path_parameter
              )
              return handler(event)
          
        • general_util.py:全機能共通のutil

      • lib
        • pip installしたライブラリ
    • cdk_stack.py:AWSリソース構築に必要

  • README.md

  • app.py:AWSリソース構築に必要

  • cdk.json:AWSリソース構築に必要

  • setup.py
    # ここにライブラリの情報を記載することが可能だが、以下の書き方にしsetup.cfgにまとめている
    
    from setuptools import setup
    
    setup()
    
  • setup.cfg:このプロジェクトをpip install 可能にする。requirements.txtの役割もあり
    [metadata]
    name = [このライブラリの名前]
    version = [X.X]
    
    [options]
    install_requires = 
    		[必要なライブラリを記載する] ※製品版requirements.txtの役割
        
    packages = find:
    package_dir =
        = src
    
    [options.packages.find]
    where = src
    
    [options.extras_require]
    develop =
    		[開発時に必要なライブラリを記載する] ※開発用requirements.txtの役割
    
    [options.entry_points]
    console_scripts =
        time = command:cli
        price = command:cli
    
  • .python-version

  • .gitignore

  • .env:環境変数を定義するファイル、git管理対象外
    [環境変数名]=[値]
    
  • source.bat

この構成の好きなところ

マイクロサービスアーキテクチャのメリットが活かせる

ポイントは以下です

  • 全ての機能の入口がhandler.pyとなっているため、入口の制御がしやすい
  • 各リソースの入口もそれぞれ実装しているため、コードレベルでリソースの制御が可能

今までは機能同士でやり取りしていることがあり、そこまで入口の統一をせず実装してしまっていることが多かったですが、実際今回のアーキにしてみると機能編集時の負担が少なくなりました

各機能の入り口はCerberusを使用し、バリデーションすることでインプットの内容の想定が容易のため、ある機能の変更によって別機能が影響を受けるかというところは判断しやすかったです

マイクロサービス勉強の際に読んだ本
https://www.oreilly.co.jp/books/9784814400010/

コマンドを実装しているため手軽に動作させることが可能

setup.cfg, setup.pyによりコマンドでpythonファイルを実行可能にし、Clickを用いてコマンドを充実させることにより、各機能をコマンドで実行可能としています

デバッグ時や各機能を使用したいときにコマンドから実行できるとAPIより動作も早いので、PCで使用するときは基本的にコマンドで動作させてます

最後に

ファイル構成・アーキテクチャに関しては、正解がないと思います

上記に記載の構成はあくまでも現状で良いと思っているものなので今後より良いファイル構成を見つけられるよう、より精進しようと思います

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
100