きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Pythonの標準ライブラリで先月今月来月の月初月末を求める(datetime, calendar)

Pythonの標準ライブラリを使って、先月今月来月の月初月末を求める方法がわからなかったので、記事にします。

なお、サードパーティ製のライブラリdateutilを使ったほうが楽に導き出せるようですが、この記事では標準ライブラリにこだわることにします。

環境

  • Python
    • 3.8.6
  • pytest
    • 5.4.3

使用する標準ライブラリ

  • datetime
  • calendar
import datetime 
import calendar

基準

次の日付を基準にします。

now = datetime.datetime(2020, 2, 15, 20, 29, 39)

今月の月初

replace(day=1)で日付を1日に変更します。

assert now.replace(day=1) == datetime.datetime(2020,2,1,20,29,39)

今月の月末

calender#monthrange()を使用します。対象の年と月をパラメータに渡し、配列の1番目を取得すると月末を取得することができます。うるう年も考慮して値を返却してくれます。

end_day = calendar.monthrange(now.year, now.month)[1]
assert now.replace(day=end_day) == datetime.datetime(2020,2,29,20,29,39)

なお、配列の0番目は月初ではありません。1日の曜日となります。ヨーロッパの慣例に従った月曜日を週の始まり(0)とし、日曜日を最後の日(6)とした値が返却されます。2020/02/01は土曜日ですので、5が返却されます。

assert calendar.monthrange(now.year, now.month) == (5, 29)
  
>>>print(calendar.month(2020, 2))
   February 2020
Mo Tu We Th Fr Sa Su
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29

先月と来月

replace(month=X, day=1)で指定した月の月初を求めることができます。

# 先月
assert now.replace(month=now.month-1 ,day=1) == datetime.datetime(2020,1,1,20,29,39)
# 今月
assert now.replace(month=now.month+1 ,day=1) == datetime.datetime(2020,3,1,20,29,39)

ただし、月は1-12の期間のみしか入力できないため、年跨ぎを行おうとするとエラーになります。

try:
    assert now.replace(month=13)
    assert False
except ValueError:
    assert True

もし、安全に処理したい場合は、1日にreplaceした後に-1日を行い、再度1日にreplaceすることで先月の月初を求められます。

last_month = (now.replace(day=1) - datetime.timedelta(days=1)).replace(day=1)
assert last_month == datetime.datetime(2020,1,1,20,29,39)

来月の月初も同様です。月末日にreplaceした後に、+1日をすることで来月末の月初を求められます。

max_day = calendar.monthrange(now.year, now.month)[1]
next_month = now.replace(day=max_day) + datetime.timedelta(days=1)

assert next_month == datetime.datetime(2020,3,1,20,29,39)

備考

先月、来月の前後1ヵ月に関しては安全に処理できます。しかし、前後2ヵ月以上の計算は標準ライブラリでは処理が面倒です。おそらく、この点においてサードパーティのdateutilを使用するのでしょう。

標準ライブラリで1ヵ月加算することが簡単だったらよかったのですが、datetime.timedeltaのリファレンスを参照する限り難しそうです。

# monthsが欲しかった…。
datetime.timedelta(
  days=0, 
  seconds=0, 
  microseconds=0, 
  milliseconds=0, 
  minutes=0, 
  hours=0, 
  weeks=0
)

ソースコード

終わりに

Pythonがまだ不慣れだということもあり、簡単に計算できると思っていたことが簡単には処理できませんでした。

というか、日付計算にadd(day=0)みたいなメソッドがあると思っていたのですが、それがないのが個人的に不満ポイント。日付計算にdatetime.timedelta(day=0)が必要だということが分かって、直感的にコーディングできると言われているPythonであればもうちょっと簡単な方法があるのではないかと疑ってしまいました。

addとかだとインスタンスが可変に見えるから導入しない、とかそういう理由ですかね…。

手間取ってしまいましたが、違う言語を学ぶことはいい経験になりますね。他の言語も学んで色んな考え方を吸収したいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

f:id:nainaistar:20210707002516p:plain

Pythonで記述されたPlaywrightの実行環境をDockerで用意する

最近、私の中でMicrosoft製のE2EツールのPlaywright(Star数25.6K)がアツイです。

今回、PlaywrightをCI環境で使いたかったため、実行環境をDockerで準備する手順を残します。また、CIでE2Eのシナリオを実行できるようにします。

環境

  • Python
    • 3.8.6
  • Playwright-Python
    • 1.12.1
  • GitHub Codespaces
    • 2021/07/10時点

ゴール

  • Playwrightの実行環境をDockerで用意する
  • Pythonで作成したシナリオをCIで実行する

書かないこと

  • Playwrightについて
  • 既存E2Eツールとの比較

手順

Dockerfileを準備する

WebDriver等が入っているDockerイメージをMicrosoftから提供されているので利用します。しかし、Playwright自体にはパスは通っていません。

ですので、Dockerfile内で自分で使用したい言語のPlaywrightをインストールする必要があります。今回は、Pythonで書かれたシナリオを動かしたいため、pip install playwrightを実行します。

FROM mcr.microsoft.com/playwright:focal
RUN pip install playwright

シナリオを準備する

動かしたいシナリオを準備します。次に記載しているシナリオは公式のサンプルをそのまま使用しています。現時点では動いていますが、最新版はどうなるか分からないため、公式を確認してください。

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        for browser_type in [p.chromium, p.firefox, p.webkit]:
            browser = await browser_type.launch()
            page = await browser.new_page()
            await page.goto('http://whatsmyuseragent.org/')
            await page.screenshot(path=f'example-{browser_type.name}.png')
            await browser.close()

asyncio.run(main())

docker-compose.ymlを準備する

Dockerfileを指定する

今回は、docker-compose up -dの実行と共にDockerfileのビルドを行いたいため、buildを使用します。docker-compose.ymlとDockerfileを同一のディレクトリに配備するので、カレントディレクトリをcontextに渡します。

services:
  playwright:
    build: 
      context: ./

シナリオをマウントする

シナリオをDockerに渡したいため、volumesで適当な場所にマウントします。また、シナリオの実行結果を受け取れるようにしておきます。

今回は、ローカルのscenarioディレクトリをDockerの/var/scenarioにマウントさせます。

services:
  playwright:
    volumes:
        - ./scenario:/var/scenario

シナリオを起動時に実行する

docker-compose.ymlのentrypointを指定すると、docker-compose up -dで実行した時に起動します。

先ほど、シナリオを/var/scenarioにマウントさせたので、ディレクトリを移動しつつ、シナリオをpython sample.pyで起動します。

services:
  playwright:
    entrypoint: >
      sh -c "
        cd /var/scenario &&
        python sample.py
      "

全てまとめる

全てまとめるとこのファイルになります。versionは好きなものを指定してください。

version: '3.8'
services:
  playwright:
    build: 
      context: ./
    volumes:
        - ./scenario:/var/scenario
    entrypoint: >
      sh -c "
        cd /var/scenario &&
        python sample.py
      "

CI環境で実行する

docker-compose up -dをしたタイミングでシナリオを起動するようにしました。

ですので、CI環境では次のコマンドを実行すれば、期待通りに実行できます。

docker-compose down
docker-compose up -d
docker-compose down

備考

起動エラーになった時のログを確認したい

docker-compose logsでログを確認することができます。

docker-compose logs

コンテナ内部でシナリオを確認したい

次のコマンドで新規にイメージを作成しながらコンテナにログインできますので、期待通りに動作していないというときは、コンテナ内で処理を確認してください。

docker-compose run playwright bash

シナリオ作成には向かない

あくまで実行環境でしかありません。

このイメージにはGUI機能等は入っていませんので、ヘッドレスモード以外での起動はできません。また、ブラウザ操作を自動でコードにしてくれるplaywright codegenもありますが、そのブラウザ自体を表示できませんので、エラーとなります。

playwright    | [pid=121][err] [121:121:0710/141616.811572:ERROR:browser_main_loop.cc(1412)] Unable to open X display.

.DevContainerのように使いたかったのですが、難しそうです。

ソースコード

終わりに

同じく優秀なE2EツールとしてはGoogle製のPuppetter(Star数71.9K)もあります。Playwrightの方が後続のツールということで、Star数に関しては追いついていませんが、後続だけあってPuppetterよりも簡単に書けるようです。

あと、このDockerからProxyを突破できるか気になりますね…。職場ではまだ導入していないので、そういう知識もブログ等に起こしておきたいと思います。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

f:id:nainaistar:20210710232420p:plain

DBからJavaのResultSetにLocalDateTimeやBigInteger等の基本型以外の型を直接設定する

TwitterやJavaのオープンチャットででたまーにResultSetから直接LocalDateTimeBigIntegerを取得できずに、一旦Stringjava.sql.Dateで取得した後に変換している、ということを見かけることがありました。直接LocalDateTimeやBigIntegerで取得することができます。

今回の記事では、DBからJavaのResultSetを駆使し、JdbcTemplate#queryにてLocalDateTimeBigIntegerを取得する方法を記載します。

環境

  • Java
    • 15
  • org.springframework.boot
    • 2.5.2
  • org.flywaydb:flyway-core
    • 7.7.3
  • com.h2database:h2
    • 2.1.2.200
  • org.springframework.boot:spring-boot-starter-test
    • 2.5.2
  • org.springframework.boot:spring-boot-starter-jdbc
    • 2.5.2
  • org.mybatis.spring.boot:mybatis-spring-boot-starter
    • 2.2.0
  • org.mybatis.spring.boot:mybatis-spring-boot-starter-test
    • 2.2.0
  • Gradle

ゴール

JavaのJdbcTemplate#queryにてRLocalDateTimeBigIntegerを取得します。

要約

ResultSetgetObjectの第二パラメータに必要なクラスを指定します。

方法

事前準備

テーブルとデータを準備します。

CREATE TABLE BOOK
(
    isbn         VARCHAR(13) PRIMARY KEY,
    money        INT,
    number       INT,
    generate_date TIMESTAMP
);

INSERT INTO BOOK VALUES ('9784798126708', 1000, 10, '2021-07-02 12:34:56')

取得用のDtoを作成する

jdbcTemplateから取得するための型を用意します。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookDto implements Serializable {
  private String isbn;
  private int money;
  private BigInteger number;
  private LocalDateTime generateDate;
}

Javaで取得する

JdbcTemplate#queryを使用します。ResultSetgetObjectの第二パラメータに必要なクラスを指定します。

今回の場合は、BigInteger.class, LocalDateTime.classを指定することができます。

protected List<BookDto> findBookList() {
  return jdbcTemplate.query("SELECT * FROM BOOK", (rs, i) ->
      BookDto.builder()
          .isbn(rs.getString("isbn"))
          .money(rs.getInt("money"))
          .number(rs.getObject("number", BigInteger.class))
          .generateDate(rs.getObject("generate_date", LocalDateTime.class))
          .build());
}

備考

単純にラップした自作型に関してはマッピングできないようです。ISBNクラスを作成してマッピングしようとしましたが、エラーログが出力されました。

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookDto implements Serializable {
  private Isbn isbn;
  private int money;
  private BigInteger number;
  private LocalDateTime generateDate;
}

@Value
@AllArgsConstructor
public class Isbn {
  private final String isbn;
}

protected List<BookDto> findBookList() {
  return jdbcTemplate.query("SELECT * FROM BOOK", (rs, i) ->
      BookDto.builder()
          .isbn(rs.getObject("isbn", Isbn.class))
          .money(rs.getInt("money"))
          .number(rs.getObject("number", BigInteger.class))
          .generateDate(rs.getObject("generate_date", LocalDateTime.class))
          .build());
}
Caused by: org.h2.jdbc.JdbcSQLFeatureNotSupportedException: 機能はサポートされていません: "kirimaru.biz.domain.book.Isbn"
Feature not supported: "kirimaru.biz.domain.book.Isbn" [50100-200]

素直に構造に合わせたRowMapperを作成しましょう。

        .isbn(new Isbn(rs.getString("isbn"))

ソースコード

単体テストも行えるようにしています。セットアップのためのFlyway等は今回の記事では解説しません。

終わりに

地味ながら、低くない頻度で見かけるので記事にしました。

やってることは簡単なので、困っている人に届くと嬉しいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

f:id:nainaistar:20210702212124p:plain