Exécuter périodiquement un script Python avec Docker Compose + Daemon (cron)

Exécuter périodiquement un script Python avec Docker Compose + Daemon (cron)

introduction

`Ceci est un article sur l'environnement Mac, mais la procédure est la même pour l'environnement Windows. Veuillez lire et essayer la partie dépendant de l'environnement. ''

Objectif

Lancez Daemon (cron) et Database (PostgreSQL) avec Docker Compose et exécutez Python Script régulièrement.

Après avoir lu cet article jusqu'au bout, vous pourrez:

No. Aperçu mot-clé
1 Docker Compose docker-compose
2 cron crond
3 SQLAlchemy sqlalchemy
4 PostgreSQL psycopg2
5 Alembic alembic

Environnement d'exécution

environnement Ver.
macOS Catalina 10.15.6
Docker 19.03.12
Python 3.7.3
PostgreSQL 11.5
alembic 1.4.2
psycopg2-binary 2.8.6
requests 2.24.0
SQLAlchemy 1.3.19

Code source

Je pense que vous pouvez approfondir votre compréhension en suivant réellement le contenu de l'implémentation et le code source. Veuillez l'utiliser par tous les moyens.

GitHub

Articles Liés

Configuration du système

scénario

Enregistrez le nombre cumulé d'accès quotidiens à Qiita.

  1. Lancez Daemon (cron) et Database (PostgreSQL) avec Docker Compose
  2. Réglez l'heure de démarrage du démon (cron) sur 23h00.
  3. Exécutez le script Python déclenché par le démarrage de Daemon (cron)
  4. Obtenez des informations Qiita avec Python Script et enregistrez-les dans PostgreSQL

Résultat d'exécution

Docker

docker.sh


qiita=# SELECT * from qiita ORDER BY date DESC, page_views_count DESC;
 id |    date    |                                   title                                   | page_views_count | likes_count |                         url                          |         created_at
----+------------+---------------------------------------------------------------------------+------------------+-------------+------------------------------------------------------+----------------------------
  1 | 2020-09-10 |Obtenir des données Salesforce à l'aide de l'API REST|            11571 |          23 | https://qiita.com/nsuhara/items/19cf8ec89b88fb3deb39 | 2020-09-10 12:48:23.512842
  2 | 2020-09-10 |Compétence Alexa en Python/Implémenté dans Lambda|             4888 |          11 | https://qiita.com/nsuhara/items/5b19cfb5ffb897bd4cfd | 2020-09-10 12:48:23.513017
  3 | 2020-09-10 |Limiter les pièces jointes Salesforce|             4611 |           6 | https://qiita.com/nsuhara/items/bd41c9ad946b8b832207 | 2020-09-10 12:48:23.51307
  4 | 2020-09-10 |Test unitaire pour Swift(Unit Test)comment faire|             4214 |          12 | https://qiita.com/nsuhara/items/bc06c07ff30a5b78696d | 2020-09-10 12:48:23.51313
  5 | 2020-09-10 |IOS avec Kony App Platform/Créer une application Android|             3612 |           3 | https://qiita.com/nsuhara/items/c28d838492512850520c | 2020-09-10 12:48:23.513182
  6 | 2020-09-10 | Flask-SQLAlchemy +Créer un service Web dans PostgreSQL|             2842 |           7 | https://qiita.com/nsuhara/items/fa5998c0b2f4fcefbed4 | 2020-09-10 12:48:23.513237
  7 | 2020-09-10 | Heroku + Selenium +Automatisez le processus WEB avec Chrome|             2719 |          15 | https://qiita.com/nsuhara/items/76ae132734b7e2b352dd | 2020-09-10 12:48:23.513292
  8 | 2020-09-10 | Docker +Créer un service Web avec Flask|             2368 |           7 | https://qiita.com/nsuhara/items/c7eff7fae3801f85b5cd | 2020-09-10 12:48:23.513343
  9 | 2020-09-10 |Exécutez régulièrement Python sur Heroku Scheduler|             2232 |           4 | https://qiita.com/nsuhara/items/fac20adb6b0a122a3709 | 2020-09-10 12:48:23.513393
 10 | 2020-09-10 | Messaging API + LIFF + Heroku + Flask +Créer LINE BOT avec l'extension Framework|             1913 |           8 | https://qiita.com/nsuhara/items/0c431913165e4af0f8f5 | 2020-09-10 12:48:23.513449
 11 | 2020-09-10 |Service de données Salesforce Lightning(LDS)en savoir plus sur|             1636 |           4 | https://qiita.com/nsuhara/items/ecd77def7aa1f985efcc | 2020-09-10 12:48:23.513503
 12 | 2020-09-10 |Créer un service Web RESTful avec Flask|             1571 |           5 | https://qiita.com/nsuhara/items/449835bc94f0fb3bbcbd | 2020-09-10 12:48:23.513553
 13 | 2020-09-10 | AWS-Lambda +Données AWS CSV avec Python-Écrire dans S3|             1565 |           2 | https://qiita.com/nsuhara/items/b2bd1d2623bca0f767f8 | 2020-09-10 12:48:23.513604
 14 | 2020-09-10 |IOS créé avec Kony App Platform/En savoir plus sur le codage des applications Android|             1536 |           1 | https://qiita.com/nsuhara/items/bf0e8884a7efc3c55176 | 2020-09-10 12:48:23.513654
 15 | 2020-09-10 |De JSON à Word en utilisant Python(docx)Créer|             1511 |           2 | https://qiita.com/nsuhara/items/3ba2fa7db38acd04f448 | 2020-09-10 12:48:23.513708
 16 | 2020-09-10 | AWS-Lambda +AWS avec Python-RDS/Lire la table PostgreSQL|             1408 |           3 | https://qiita.com/nsuhara/items/dd780c2622258d10f961 | 2020-09-10 12:48:23.51376
 17 | 2020-09-10 | AWS-Lambda + Python +Effectuer régulièrement du grattage WEB avec Cron|             1342 |           5 | https://qiita.com/nsuhara/items/0d36600511fc162827f6 | 2020-09-10 12:48:23.513819
 18 | 2020-09-10 | Django + SQLAlchemy + SQLite3 /Créer une application Web dans PostgreSQL|             1287 |           3 | https://qiita.com/nsuhara/items/4ab5366273082ee0aa73 | 2020-09-10 12:48:23.513874
 19 | 2020-09-10 |IOS créé avec Kony App Platform/Lien de données entre l'application Android et Salesforce|             1088 |           1 | https://qiita.com/nsuhara/items/756120f1bddc6f8fe78b | 2020-09-10 12:48:23.513928
 20 | 2020-09-10 |IOS créé avec Kony App Platform/En savoir plus sur la disposition automatique pour les applications Android|              898 |           0 | https://qiita.com/nsuhara/items/a52abd9861c51823ecec | 2020-09-10 12:48:23.513984
 21 | 2020-09-10 |AWS avec Python-S3 signé(Avec une limite de temps)Générer une URL|              723 |           2 | https://qiita.com/nsuhara/items/20160b080c2b30d57729 | 2020-09-10 12:48:23.514131
 22 | 2020-09-10 |Interface graphique de la passerelle API Kong/Construire Konga|              665 |           3 | https://qiita.com/nsuhara/items/a0de75e6767f98cc8fec | 2020-09-10 12:48:23.51419
 23 | 2020-09-10 |Créer une voiture à distance avec Raspberry Pi et Python|              637 |           2 | https://qiita.com/nsuhara/items/7970b5dfe95ea76c93d6 | 2020-09-10 12:48:23.514248
 24 | 2020-09-10 |Construire la passerelle API Kong|              535 |           1 | https://qiita.com/nsuhara/items/ad1d8fa1faad7940b5c1 | 2020-09-10 12:48:23.514297
 25 | 2020-09-10 |De la configuration du Raspberry Pi à l'installation de l'environnement Python|              529 |           0 | https://qiita.com/nsuhara/items/05a2b41d94ced1f54316 | 2020-09-10 12:48:23.514365
 26 | 2020-09-10 |LCD avec Raspberry Pi et Python(16x2)Créer un jeu|              424 |           0 | https://qiita.com/nsuhara/items/57105fd232feffbcd05c | 2020-09-10 12:48:23.514416
 27 | 2020-09-10 |Créez une caméra de surveillance WEB avec Raspberry Pi et OpenCV|              392 |           0 | https://qiita.com/nsuhara/items/37fcbc9d0e8209080032 | 2020-09-10 12:48:23.514466
 28 | 2020-09-10 | Flask-Comment implémenter UPSERT avec SQL Alchemy|              389 |           0 | https://qiita.com/nsuhara/items/86570f789093222252b1 | 2020-09-10 12:48:23.514518
 29 | 2020-09-10 | Qiita API +Obtenez des visites d'articles et des likes en Python|              332 |           0 | https://qiita.com/nsuhara/items/b27b84f0150c3f6534ec | 2020-09-10 12:48:23.514569
 30 | 2020-09-10 |Comment insérer par lots des données CSV dans PostgreSQL|              145 |           0 | https://qiita.com/nsuhara/items/a1b75e0557ed134c5302 | 2020-09-10 12:48:23.514635
(30 rows)

Heroku

2020-09-10.png

Configuration de l'application

tree.sh


/
├── Dockerfiles
│   ├── app
│   │   ├── Dockerfile
│   │   ├── crontab
│   │   ├── docker-compose.yml
│   │   ├── entrypoint.sh
│   │   └── qiita.sh
│   └── docker_compose_up.sh
└── app
     ├── __init__.py
     ├── alembic.ini
     ├── config
     │   └── qiita_access_token
     ├── main.py
     ├── migration
     │   ├── README
     │   ├── env.py
     │   ├── script.py.mako
     │   └── versions
     │       └── 62fb1bcf0a8a_create_table.py
     ├── model
     │   ├── __init__.py
     │   └── qiita.py
     └── requirements.txt

Hands-on

Télécharger

command.sh


~% git clone https://github.com/nsuhara/python-docker-cron.git -b master

Début du service

command.sh


~% cd python-docker-cron/
~% sh Dockerfiles/docker_compose_up.sh

Fin de service

command.sh


~% Control Key + C

Python Script

tree.sh


/
└── app
     ├── main.py
     └── model
          ├── __init__.py
          └── qiita.py

Création de modèle

qiita.py


"""app/model/qiita.py
"""
from datetime import datetime, timezone

from sqlalchemy import Column
from sqlalchemy.dialects import postgresql as db
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Qiita(Base):
    """Qiita
    """
    __tablename__ = 'qiita'

    id = Column(db.INTEGER, primary_key=True, autoincrement=True)
    date = Column(db.DATE, nullable=False)
    title = Column(db.VARCHAR(255), nullable=False)
    page_views_count = Column(db.INTEGER, nullable=False)
    likes_count = Column(db.INTEGER, nullable=False)
    url = Column(db.VARCHAR(255), nullable=False)
    created_at = Column(db.TIMESTAMP, nullable=False)

    def __init__(self, date, title, page_views_count, likes_count, url):
        self.date = date
        self.title = title
        self.page_views_count = page_views_count
        self.likes_count = likes_count
        self.url = url
        self.created_at = datetime.now(timezone.utc)

    def to_dict(self):
        """to_dict
        """
        return {
            'id': self.id,
            'date': self.date,
            'title': self.title,
            'page_views_count': self.page_views_count,
            'likes_count': self.likes_count,
            'url': self.url,
            'created_at': self.created_at
        }

Création principale

main.py


"""app/main.py
"""
import os
import sys
from datetime import datetime

import requests
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from model.qiita import Qiita


def get(url):
    """get
    """
    qiita_access_token = os.getenv('QIITA_ACCESS_TOKEN', '')
    if qiita_access_token == '':
        print('Error: please set \'QIITA_ACCESS_TOKEN\' to environment variable.')
        sys.exit()

    headers = {
        'Content-Type': 'application/json',
        'Charset': 'utf-8',
        'Authorization': 'Bearer {}'.format(qiita_access_token)
    }
    res = requests.get(url=url, headers=headers)
    print('{}, {}'.format(res.status_code, res.url))
    return res


def get_id_list():
    """get_list
    """
    res = get(url='https://{url}?page={page}&per_page={per_page}'.format(**{
        'url': os.getenv('QIITA_URL_LIST'),
        'page': os.getenv('QIITA_PAGE'),
        'per_page': os.getenv('QIITA_PER_PAGE')
    }))
    return [item.get('id') for item in res.json()]


def get_item(qiita_id):
    """get_item
    """
    res = get(url='https://{url}/{id}'.format(**{
        'url': os.getenv('QIITA_URL_ITEM'),
        'id': qiita_id
    }))
    item = res.json()
    return {
        'page_views_count': item.get('page_views_count'),
        'likes_count': item.get('likes_count'),
        'title': item.get('title'),
        'url': item.get('url')
    }


def output_db(items):
    """output_db
    """
    engine = create_engine(
        os.getenv('SQLALCHEMY_DATABASE_URI', ''),
        echo=bool(int(os.getenv('SQLALCHEMY_ECHO', '0'))),
        pool_size=int(os.getenv("SQLALCHEMY_POOL_SIZE", '5')),
        max_overflow=int(os.getenv("SQLALCHEMY_MAX_OVERFLOW", '10')),
        pool_timeout=int(os.getenv("SQLALCHEMY_POOL_TIMEOUT", '30'))
    )
    create_session = sessionmaker(bind=engine)
    session = create_session()

    insert = list()
    date = datetime.now().date()

    session.query(Qiita).filter(Qiita.date == date).delete()

    for item in items:
        insert.append(Qiita(
            date=date,
            title=item.get('title'),
            page_views_count=item.get('page_views_count'),
            likes_count=item.get('likes_count'),
            url=item.get('url')
        ))

    session.add_all(insert)
    session.commit()


def output_log(items):
    """output_log
    """
    total_page_views_count = 0
    total_likes_count = 0

    print('-'*100)

    for item in items:
        total_page_views_count = total_page_views_count + \
            item.get('page_views_count')
        total_likes_count = total_likes_count+item.get('likes_count')

        print('page_views_count={}, likes_count={}, title={}, url={}'.format(
            str(item.get('page_views_count')).zfill(5),
            str(item.get('likes_count')).zfill(2),
            item.get('title'),
            item.get('url'))
        )

    print('\nitems_count={}, total_page_views_count={}, total_likes_count={}'.format(
        len(items), total_page_views_count, total_likes_count))

    print('-'*100)


def main():
    """main
    """
    items = list()

    for qiita_id in get_id_list():
        items.append(get_item(qiita_id=qiita_id))

    sorted_items = sorted(
        items, key=lambda x: x['page_views_count'], reverse=True)

    if os.getenv('OUTPUT', '') == 'output_db':
        output_db(items=sorted_items)
    else:
        output_log(items=sorted_items)


if __name__ == '__main__':
    main()

Paramètre du jeton d'accès Qiita

command.sh


~% echo "export QIITA_ACCESS_TOKEN={qiita access token}" > app/config/qiita_access_token

--Remarque: [Jeton d'accès](https://qiita.com/nsuhara/items/b27b84f0150c3f6534ec#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83 % 88% E3% 83% BC% E3% 82% AF% E3% 83% B3)

Database Migration

tree.sh


/
└── app
     ├── alembic.ini
     └── migration
          ├── README
          ├── env.py
          ├── script.py.mako
          └── versions
              └── 62fb1bcf0a8a_create_table.py

Installation d'alambic

command.sh


~% pip install alembic

Initialisation de l'alambic

command.sh


~% alembic init {directory}

Paramètres de l'alambic

command.sh


~% vim app/alembic.ini

command.sh


- sqlalchemy.url = driver://user:pass@localhost/dbname
+ sqlalchemy.url = postgresql+psycopg2://postgres:postgres@docker_db:5432/qiita

Création de script

command.sh


~% alembic revision -m 'Create Table'

--Remarque: app / migration / versions / {ID de révision} _create_table.py est généré

Édition de script

create_table.py


"""Create Table
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql as db

# revision identifiers, used by Alembic.
revision = '62fb1bcf0a8a'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    op.create_table(
        'qiita',
        sa.Column('id', db.INTEGER, primary_key=True),
        sa.Column('date', db.DATE, nullable=False),
        sa.Column('title', db.VARCHAR(255), nullable=False),
        sa.Column('page_views_count', db.INTEGER, nullable=False),
        sa.Column('likes_count', db.INTEGER, nullable=False),
        sa.Column('url', db.VARCHAR(255), nullable=False),
        sa.Column('created_at', db.TIMESTAMP, nullable=False),
    )


def downgrade():
    pass

Génération de table manuelle

command.sh


~% python app/migration/versions/{revision id}_create_table.py

Docker Compose

tree.sh


/
├── Dockerfiles
│   ├── app
│   │   ├── Dockerfile
│   │   ├── crontab
│   │   ├── docker-compose.yml
│   │   ├── entrypoint.sh
│   │   └── qiita.sh
│   └── docker_compose_up.sh
└── app

docker_compose_up.sh

docker_compose_up.sh


#!/bin/sh

docker stop $(docker ps -q)
docker rm $(docker ps -q -a)
# docker rmi $(docker images -q) -f

rsync -av --exclude=app/tests* app Dockerfiles/app
docker-compose -f Dockerfiles/app/docker-compose.yml up --build

docker-compose.yml

docker-compose.yml


version: '3'
services:
  docker_db:
    image: postgres:11.5
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: 'postgres'
      POSTGRES_PASSWORD: 'postgres'
      POSTGRES_DB: 'qiita'
      POSTGRES_INITDB_ARGS: '--encoding=UTF-8'

  app:
    build: .
    depends_on:
      - docker_db
    volumes:
      - ./crontab:/var/spool/cron/crontabs/root
    environment:
      PYTHONPATH: '/code/'
      TZ: 'Asia/Tokyo'
      QIITA_PAGE: '1'
      QIITA_PER_PAGE: '100'
      QIITA_URL_ITEM: 'qiita.com/api/v2/items'
      QIITA_URL_LIST: 'qiita.com/api/v2/authenticated_user/items'
      OUTPUT: 'output_db'
      SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://postgres:postgres@docker_db:5432/qiita'
      SQLALCHEMY_ECHO: '1'
      SQLALCHEMY_POOL_SIZE: '5'
      SQLALCHEMY_MAX_OVERFLOW: '10'
      SQLALCHEMY_POOL_TIMEOUT: '30'

Dockerfile

FROM python:3.7

RUN mkdir /code
WORKDIR /code

ADD entrypoint.sh /code/entrypoint.sh
ADD qiita.sh /code/qiita.sh
ADD app/ /code

RUN export -p
RUN chmod +x /code/entrypoint.sh
RUN chmod +x /code/qiita.sh

RUN apt-get update && apt-get -y install busybox-static

RUN pip install --upgrade pip --no-cache-dir
RUN pip install -r requirements.txt --no-cache-dir

RUN touch /var/log/cron.log

# EXPOSE 0000

CMD ["/code/entrypoint.sh"]

entrypoint.sh

entrypoint.sh


#!/bin/bash

sleep 5
# set qiita access token to environment variable
source /code/config/qiita_access_token
# migrate database
alembic upgrade head
# run script at 23:00, 0 23 * * * /code/qiita.sh  >> /var/log/cron.log 2>&1
busybox crond -l 8 -L /dev/stderr -f

crontab

0 23 * * * /code/qiita.sh  >> /var/log/cron.log 2>&1

qiita.sh

qiita.sh


#!/bin/sh

python /code/main.py

Démarrez divers services

command.sh


~% sh Dockerfiles/docker_compose_up.sh

Connectez-vous localement au conteneur Docker

command.sh


docker ps
# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
# 0af68012c7c9        app_app             "/code/entrypoint.sh"    38 seconds ago      Up 37 seconds       0.0.0.0:8000->8000/tcp   app_app_1
# 9901a105e8d5        postgres:11.5       "docker-entrypoint.s…"   39 seconds ago      Up 37 seconds       0.0.0.0:5432->5432/tcp   app_postgres_1

docker exec -i -t {CONTAINER ID} /bin/bash

Connectez-vous localement à Docker-PostgreSQL

command.sh


~% psql -h localhost -p 5432 -d qiita -U postgres

Bonus: Heroku

Créer runtime.txt

Supported runtimes

command.sh


~% cd python-docker-cron/
~% echo python-3.7.9 > app/runtime.txt

Inscription Heroku Postgres

  1. {app-name}> onglet Resources> Entrez et sélectionnez Heroku Postgres dans la recherche de modules complémentaires> bouton Provision

Paramètres des variables d'environnement

  1. {app-name}> onglet Settings> bouton Reveal Config Vars> Enregistrer les variables d'environnement

Modifier alembic.ini

command.sh


~% cd python-docker-cron/
~% vim app/alembic.ini

command.sh


- sqlalchemy.url = postgresql+psycopg2://postgres:postgres@docker_db:5432/qiita
+ sqlalchemy.url = {SQLALCHEMY_DATABASE_Même URL que l'URI}

Déployer

command.sh


~% cd python-docker-cron/app/
~% git init
~% git commit -am "make it better"
~% heroku login
~% heroku git:remote -a {app-name}
~% git push heroku master

Heroku Postgres Migration

command.sh


~% cd python-docker-cron/
~% heroku run python migration/versions/62fb1bcf0a8a_create_table.py

or

command.sh


~% heroku pg:psql {database} --app {app-name}

command.sh


{app-name}::DATABASE=> CREATE TABLE qiita (
    id SERIAL NOT NULL,
    date date not null,
    title VARCHAR(255) NOT NULL,
    page_views_count Integer NOT NULL,
    likes_count Integer NOT NULL,
    url VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL,
    PRIMARY KEY (id)
);

Enregistrement Heroku Scheduler

  1. {app-name}> onglet Ressources> Rechercher des modules complémentaires et sélectionner le bouton Heroku Scheduler> Provision
  2. Heroku Scheduler> bouton Add Job
  3. Réglez le cycle d'exécution sur «Planifier»
  4. Définissez python main.py sur Exécuter la commande

--Note 1: Exécution périodique de Python avec Heroku Scheduler --Note 2: enregistrement de la carte de crédit requis

Recommended Posts

Exécuter périodiquement un script Python avec Docker Compose + Daemon (cron)
Utiliser GDAL avec Python avec Docker
Créer un environnement Node.js avec Docker Compose
WordPress avec Docker Compose sur CentOS 8
Construire un environnement Rails 6 + MySQL avec Docker compose
[Docker] [18116] Échec de l'exécution du script de résolution de suivi de docker-compose