やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

(Power Automate)OData フィルタで日時を指定する方法について

先日、Power Automate で OData のフィルタを指定する際に日時を条件に設定する必要がありましたが、その指定方法にハマったのでメモしておきます。

ちなみに OData は Microsoft Graph API の操作を行うためのクエリ言語です。

docs.microsoft.com

環境

やろうとしたことは「SharePoint のイベントリストに登録したデータから特定の期間のデータを取得する」というものです。

例えば以下のようなイベントが登録されているものとします。

f:id:iyemon018:20201010160954p:plain

ここから、2020年10月10日~2020年10月12日までのイベントを取得するイメージです。

まずはフローを実行する

f:id:iyemon018:20201010161328p:plain

イベントを取得する場合は SharePoint の"複数の項目の取得"から取得します。 なお、イベントはリスト名の候補には出てこないのでカスタム入力する必要があります。

問題はこの"フィルター クエリ"の箇所です。 まずは、フィルター クエリを指定せずに実行した結果の JSON を見てみましょう(不要な部分は省略しています)。

{
   "statusCode":200,
   "headers":{
        //省略
   },
   "body":{
      "value":[
         {
            "@odata.etag":"\"1\"",
            "ItemInternalId":"2",
            "ID":2,
            "Title":"テスト10月10日",
            "EventDate":"2020-10-10T00:00:00Z",
            "EndDate":"2020-10-10T23:59:00Z",
            "EventType":0,
            "Modified":"2020-10-10T07:08:30Z",
            "Created":"2020-10-10T07:08:30Z",
            // 省略
         },
         {
            "@odata.etag":"\"1\"",
            "ItemInternalId":"3",
            "ID":3,
            "Title":"Test記念日",
            "EventDate":"2020-10-12T00:00:00Z",
            "EndDate":"2020-10-12T23:59:00Z",
            "EventType":0,
            "Modified":"2020-10-10T07:09:02Z",
            "Created":"2020-10-10T07:09:02Z",
            // 省略
      ]
   }
}

これを見るとEventDateが開始日時、EndDateが終了日時であることが分かります。 なので、今回の要件を満たすにはフィルター クエリには「2020年10月10日 <= 開始日時 <= 2020年10月12日」を指定すれば良いですね。

ちなみになんでフィルター クエリなのか?というとこっちのほうが実行時間が短いからです(多分)。 例えば、フィルター クエリを指定せずに全データを取得してから Apply to each で必要な期間だけ取得することもできますが、データ量が増えてくるとどうしても時間がかかります。 特に Power Automate は。 この時必要になる時間は(正確には検証していませんが)フィルター クエリを指定したほうが短いです。 Apply to each を使う場合、データを削除しない限りは実行時間が短くなることは無いので、この方法を採用しています。

フィルター クエリを設定する

ドキュメントを見ると次のような論理演算子がサポートされています。

  • eq: 等しい
  • ne: 等しくない
  • lt: より小さい
  • gt: より大きい
  • le: 以下
  • ge: 以上
  • and: かつ
  • or: または
  • in: 含まれる
  • not: 否定

上記のフィルター条件を指定する場合、は次のようになります。

EventDate ge 2020-10-10T00:00:00Z and EventDate le 2020-10-12T00:00:00Z

では実行しましょう。

f:id:iyemon018:20201010162751p:plain

はい。失敗しますね。

エラー詳細には次のようなメッセージが出力されると思います。

式 "EventDate ge 2020-10-10T00:00:00Z and EventDate le 2020-10-12T00:00:00Z" が無効です。 Creating query failed.
clientRequestId: <id>
serviceRequestId: <id>

この問題を修正するには次のようにフィルター クエリを修正します。

EventDate ge '2020-10-10T00:00:00Z' and EventDate le '2020-10-12T00:00:00Z'

変わった箇所は、日時を ' (シングルクォーテーション)でくくっているところです。 これを実行するとフローは成功します。

f:id:iyemon018:20201010163139p:plain

スケジュールを指定して実行している場合は以下のように変数に日時を保存しておいてフィルターを指定するのが良いと思います。

formatDateTime(variables('FromDateTime'), 'yyyy-MM-ddT00:00:00Z')


SharePoint などでリストから特定の情報をフィルタすることはよくあると思います。 日時が含まれるケースは少ないかもしれませんが、そういった場合こそハマりがちなので参考にして頂ければ。

(Power Automate)Plannerに登録したタスクの期限が近い場合に通知するフローを作成する

現在、私が所属しているチームではタスク管理ツールにMicrosoft Plannerを使用しています。 単純な親子関係のないタスクであれば簡単に管理できて、チェックリストや期限の設定もでき、進捗状況や割当状態をグラフやカレンダーで可視化することができます。 また、Web会議は Teams を主に使用していることもあり、連携するならこれが今の所ベストな選択肢となっています。 感覚的には Trello と同じように使えるのもポイントです。

しかし、Planner そのものに期限を通知するための仕組みはありません。 そのため、業務で利用する際に「遅延しているタスク」がモリモリ増えていって、このままではタスク管理している意味がない!と思い、Power Automate で毎日通知するようにしました。

今回はその方法についてまとめておきます。

環境

  • Power Automate
  • Planner
  • チャットツール

チャットツールは何でも良いです。Slack や Teams ならコネクタがあるのでそちらを使いましょう。 弊社では Chatwork を使っていたのでカスタムコネクタを作ってメッセージを送るようにしました。

今回は Power Automate がメインで、カスタムコネクタや Planner についての話はしません。

やりたいこと

やりたいことは以下の3つです。

  1. 当日が期限のタスク一覧を通知する
  2. その週が期限のタスク一覧を通知する
  3. 遅延しているタスク一覧を通知する

実際のところ、この3つは通知が必要なタスクかどうかの判断以外はすべて同じなので1つだけ説明して、他は差分箇所のみ紹介します。 そしてこれを実行すると以下のようなメッセージが表示されます。

f:id:iyemon018:20200913165236p:plain

通知するメッセージには以下の情報を記載します。

  • 期限
  • タスクタイトル
  • 割り当てられた担当者の名前

フローを作成する

フローは次のような構成になっています。 全体のざっくりとした流れはスケジュール実行→Plannerからタスク取得→通知用メッセージ初期化→条件に一致するタスクを収集・メッセージの整形→チャットに送信となっています。 詳細は番号にそって説明します。

f:id:iyemon018:20200913165945p:plain

1. スケジュールの設定

説明するまでもないですね。通知したい時間を設定します。 タスクの通知なので、できれば朝一番か業務開始直前とかが良いと思います。

2. タスクを一覧表示します

Planner から該当するグループ・プランのタスクを収集します。 こちらも説明は不要ですね。 取得したいグループ・プランIDを指定するだけです。

3. 変数を初期化する/4. 変数を初期化する 2

今回、変数は2つ用意しました。 1つは期限であるタスク一覧を通知するためのメッセージで、もう1つは実際に通知するメッセージです。

なぜこんな回りくどいやり方をするかと言うと、今日が期限のタスクが0件の場合にどう通知するかを考慮したためです。

今日が期限のタスクが0件の場合、通知された側としては何が表示されていればいいでしょうか? 今回のケースでは「今日が期限のタスクは0件です。」と通知するようにしました。

仮に、「通知しない」という選択をした場合、フローが動いているのかどうか通知を見た側はわかりません。 そうなるとフローが間違っているのか期限になるタスクが無いのか判断できません。 そうなるよりは通知は増えますが、「0件です」と表現したほうがわかりやすいと思います。 (チームによってどう通知するべきかは変わるのでここは調整してください)

この要件を満たしたいので、「3. 変数を初期化する」に期限が今日であるタスク一覧の通知用メッセージを代入し、「4. 変数を初期化する 2」に実際に通知するメッセージを代入します。

5. タスク一覧を収集してメッセージを整形する

ここが一番のメインです。収集したタスクを条件に一致するかどうか判断し、一致する場合はメッセージを作成します。 収集するタスクは複数あるのでループで処理していきます。 更に詳しく見ていきます。

f:id:iyemon018:20200913171214p:plain

ポイントは2つ。「a. 通知すべきタスクかどうかを判断する」の条件と「b. 割当先ユーザーごとにメッセージを整形する」のメッセージ整形部分です。

a.通知すべきタスクかどうかを判断する

ここでは、「今日が期限のタスクかどうか」を判断します。 条件式には次のものを設定しています。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>
    Planner では期限dueDateTimeは空欄でも許容されます。空欄の場合はこれ以降の条件式で比較した場合、エラーになるためこの条件を先頭に持ってきています。
  • <値 達成率>次の値未満<100>
    Planner の達成率は数値で表現され、範囲は 0 ~ 100 となっています。100 だと達成してしまうため、100 未満としています。
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以上<ticks(addDays(utcNow(), 0))>
    期限が UTC の現在日時以降かどうかを判断しています。わざわざaddDaysで 0 日を加算しているのは、この後の条件式と比較しやすいためです。 また、判定する期間を変える場合(例えば今週期限のタスクを収集するといったケース)、0 の部分を変更するだけでいいので少し楽できます。
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値未満<ticks(addDays(utcNow(), 1))>
    期限が UTC の現在日付+1日以前かどうかを判断しています。上の条件と組み合わせて"今日"という条件を作り出しています。

b.割当先ユーザーごとにメッセージを整形する

条件に一致したタスクを収集できたら次はメッセージの整形です。 Planner では、1つのタスクに対して複数ユーザーを割り当てることができます。 通知するのであればタスクに割り当てられた全員に通知するべきです。

通知メッセージの内容は要件次第ですが、今回は1つのタスクに複数ユーザーが割り当てられた場合、ユーザーごとに1行メッセージを追加していくことにしています。 「割当先ユーザーごとにメッセージを整形する」がループになっているのはそのためです。 このループでは割り当て先ユーザーIDを値としています。 実は、Planner でタスクを収集しても割り当てられたユーザー名は取得できません。 そこで、ユーザーIDを使います。

このユーザーIDは、Microsoft 365 の ID に紐付けられています。 なので、Microsoft 365 の「ユーザー プロフィールの取得(V2)」を選択し、"ユーザー(UPN)"に Planner から取得したユーザーID を指定してやればOKです。

そうすればメッセージの整形に取得したユーザーの表示名を使用することができます。

6. 期限が今日のタスクがあるか判定する

ここまでで、期限が条件に一致するタスクを通知するためのメッセージを作っているので、そのメッセージが空であれば「タスクが無いことを通知する」。そうでなければ「条件に一致したタスク情報一覧を通知する」ようにしています。

f:id:iyemon018:20200913173159p:plain

7. チャットに新しいメッセージを追加する

ここは説明するまでもないですが、メッセージを特定のチャットルームに通知しています。 できれば、どのフローから通知されているかもメッセージに含めておくと良いでしょう。

iyemon018.hatenablog.com

期限を変えてみる

フローが作成できたので、他の期限を設定します。 フローを別名保存しておきましょう。

ここで大事なのが、期限ごとのフローで同じタスクを通知しないというところです。 制御するためにはa.通知すべきタスクかどうかを判断するの条件を変えてやればOKです。

その週が期限のタスク一覧を通知する

条件は単純です。「通知の翌日からその週の最終日まで」とすれば良いのです。 具体的には、次のとおりです。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>(変更なし)
  • <値 達成率>次の値未満<100>(変更なし)
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以上<ticks(addDays(utcNow(), 1))
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値未満<ticks(addDays(utcNow(), 6))

太字にした箇所を UTC 現在日時の1日後~6日後で期限の日付を比較しています。ここでticks(addDays(utcNow(), 0))としてしまうと、今日が期限のタスクも取得できてしまいます。

遅延しているタスク一覧を通知する

こちらは更に単純です。

  • <empty(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime'])>次の値に等しい<false>(変更なし)
  • <値 達成率>次の値未満<100>(変更なし)
  • <ticks(items('タスク一覧を収集してメッセージを整形する')?['dueDateTime']) >次の値以下ticks(addDays(utcNow(), 0))

タスクの期限が UTC の現在日時よりも過去の日時かどうかを判断しているだけです。


Planner は通知機能が弱いので Power Automate で良い感じに通知してあげるとかなり使い勝手が良くなります。 もっと良い方法をご存じの方は是非コメントください!

(Power Automate)実行中のフロー名を関数を使って取得する

小ネタです。 Power Automate で特定のチャットツールにメッセージを送るようなフローをよく作るのですが、フローが増えてくるとどれで送ってるんだっけ?となりがちです。 個人的によくやる手として以下のような一文を最後につけて送信しています。

(このメッセージは Power Automate - <フロー名> によって送信されました。)

こうすることで「どのツールの」「どの機能を使っているか」を明確にすることができます。 ただ、フロー名を後で変更した場合など、固定値を入力していると変更し忘れて意味をなさなくなる可能性があります。 できれば、フロー名の変更にも追従したい&コピペできるようにしたい!

そんな場合は次の関数を呼び出せばOKです。

workflow()?['tags']?['flowDisplayName']

これをメッセージに埋め込んでおけばフロー名の変更にも対応できます。

ちなみに以下のリファレンスで記載されているworkflow().run.nameを呼び出すとフローのIDが取得できます。

docs.microsoft.com

初見だとこっちのほうがフロー名ぽいんですけどね。

Azure DevOps REST APIで大量のPull Requestを取得する際の注意点について

先日、Azure Repos で管理しているリポジトリの Complete 済みの全 Pull Request を取得したいという要件があったので Azure DevOps REST API を使いました。 その時、条件の指定方法によっては取得できる Pull Request の数が想定より少ない数しか取得できない、という現象が発生しました。 回避方法はあったのですが、割と使われやすい API であり、今後もハマリポイントになりそうなので備忘録として残しておきます。

今回使用する Azure DevOps REST API はこちら。

docs.microsoft.com

開発環境

今回は C# でしか検証していません。他の言語などでは挙動が変わる可能性があるのでご了承ください。 また、 Visual Studio CodeREST API でも試しましたが、こちらの場合は応答はありましたが結果が表示されないという現象が発生しました。原因については分かっていません。

問題点

まずはコードを。事前に NuGet でNewtonsoft.JsonSystem.Net.Httpを追加しておきます。

using System;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;
using System.Net.Http.Headers;

namespace AzureDevOps.Api
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(":<Personal Access Token>"));            
            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);

                HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, $"https://dev.azure.com/<organization>/<project>/_apis/git/repositories/<repository>/pullrequests?api-version=6.0");
                HttpResponseMessage response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseContentRead);
                if (!response.IsSuccessStatusCode)
                {
                    throw new HttpRequestException(response.RequestMessage.ToString());
                }

                string json = await response.Content.ReadAsStringAsync();
                dynamic pullRequests = Newtonsoft.Json.JsonConvert.DeserializeObject(json);

                System.Console.WriteLine($"Pull-Requestの数:{pullRequests.count}");
            }
        }
    }
}

細かい説明は省きますが、目的は API で指定したリポジトリの Pull Request 数を出力しています。 検証対象の環境では Pull Request の数が 2000 を超えているものを使用しました。

この時点では、以下のように何の条件も指定していません。

https://dev.azure.com/<organization>/<project>/_apis/git/repositories/<repository>/pullrequests?api-version=6.0

これを実行すると以下のような結果が出力されます。

Pull-Requestの数:62

期待値としては 1000 以上ですが、そうはなっていません。

原因

ググると同じ問題にハマっている人がすぐに見つかりました。

stackoverflow.com

どうやらこの振る舞い自体は API 側の仕様のようで、条件が指定されていない場合、何千件もの Pull Request を返すことになるため返却される件数が少なくなっている(らしい)とのことです。 つまり、対策は上記のページにも書いてあるとおり、条件を指定して返却される件数を指定してやれば良いのです。

対策

今回のように Complete 済みの全 Pull Request を取得するといった要件の場合、次の URI パラメータを使用します。

  • $skip
  • $top
  • searchCriteria.status

実際の URI は次のようになります。

https://dev.azure.com/<organization>/<project>/_apis/git/repositories/<repository>/pullrequests?$skip=n&$top=1000&searchCriteria.status=completed&api-version=6.0

$skip=nとしているのは複数回 API を呼び出さなければならないためです。 さて、これに対応したコードは次のようになりました。

using System;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;
using System.Net.Http.Headers;

namespace AzureDevOps.Api
{
    class Program
    {
        static async Task Main(string[] args)
        {
            int totalCount = 0;
            string auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(":<Personal Access Token>"));            
            using (HttpClient httpClient = new HttpClient())
            {
                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);

                int numberOfCalls = 0;
                do
                {
                    HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, $"https://dev.azure.com/<organization>/<project>/_apis/git/repositories/<repository>/pullrequests?$skip={numberOfCalls * 1000}&$top=1000&searchCriteria.status=completed&api-version=6.0");
                    HttpResponseMessage response = await httpClient.SendAsync(message, HttpCompletionOption.ResponseContentRead);
                    if (!response.IsSuccessStatusCode)
                    {
                        throw new HttpRequestException(response.RequestMessage.ToString());
                    }

                    string json = await response.Content.ReadAsStringAsync();
                    dynamic pullRequests = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
                    int count = (int)pullRequests.count;                        

                    if (count == 0) break;

                    totalCount += count;
                    numberOfCalls += 1;
                } while (true);
            }

            System.Console.WriteLine($"Pull-Requestの数:{totalCount}");
        }
    }
}

これで 1000 件を超える Pull Request の取得にも対応できます。

おまけ

今回作成したコードで API の条件に指定した件数によって実行時間がどのくらい変わるのか試してみました。 件数は 100 / 500 / 1000 で3回実施しました。

回数 100件 500件 1000件
00:00:21.7547365 00:00:39.0122263 00:00:36.1842337
00:00:32.3838879 00:00:38.4844625 00:00:34.9170869
00:00:32.5571534 00:00:38.5509034 00:00:28.5829396

微妙なところですが、まぁ、誤差の範囲かなーと思います。 取得件数による応答時間はあまり変わらないので、取得する際の要件次第で1回あたりの取得件数は変えるのが良さそうです。

在宅勤務になったのでコーヒーミルを買いました

私の勤務している会社は緊急事態宣言に伴い在宅勤務となりました。 普段から勤務中は個包装ドリップタイプのコーヒーを飲んでいるのですが、それらは全部会社に置き忘れてしまったので、せっかくだしコーヒーミルを買って豆を挽いて飲むようにしました。

今回は新しく買ったコーヒーミルを紹介していきたいと思います。

買ったもの

買ったのは HARIO の手動ミルです。 手動ミルを買ったのは初めてでしたが、レビュー数が多く安定していたのとまとまった量の豆を挽きたかったのでこちらを選択しました。

このミルは挽いた豆をそのまま蓋をして保存できるのですが、100g とか 200g 挽いてるとさすがにたりなさそうなのでミニキャスターも購入しました。

なお、HARIO のコーヒーフィルターとドリッパーは形状が円錐になっているので、スーパーなどでは同型のものが売り場においていない可能性があるのでご注意ください。

私はもともと 60V のドリッパーは持っていたのですが、持っていない方はこちらも購入したほうが良いと思います。

無事必要なものが到着したので早速豆を挽いてみました。

予想はしていましたが、200g 挽くのに 30分くらいかかりました。 普段からインドア派な私にはなかなかの重労働です。。。 在宅勤務のちょっとした運動と考えれば結構いい感じなんですけどね。

それでもやっぱり挽きたてのコーヒーは香りが良く、それだけで気分が高揚します。 ついでにミニキャスターもインテリアとしてもなかなかに優秀です。計量スプーンがいい味出してます。

f:id:iyemon018:20200427135124p:plain

コーヒー豆ごとに色違いのキャスターを使うとより一層おしゃれになります。 (背景が残念ですが)

f:id:iyemon018:20200427135512p:plain

HARIO (ハリオ) V60 計量スプーン Wood M-12WD

HARIO (ハリオ) V60 計量スプーン Wood M-12WD

  • メディア: ホーム&キッチン

注意点

このコーヒーミルは豆の粗さを調節することができます。 取説ではメモリを調節することで、豆の粗さを自由に変えられますと記載されていましたが、ぶっちゃけどれがメモリかわかりませんでした!

で、色々試した結果、どうやら以下の赤いところがメモリらしいです。

f:id:iyemon018:20200427140343p:plain

流石に初見でこれはわからんよ…

いくつか試してみた

コーヒーミルを買ってから以下の豆を試しに飲んでみました。

個人的にはキリマンジャロのほうが好きですがちょっとお高め。 UCC コーヒーは値段の割に美味しいので、よく飲む人は買っておいて損は無いでしょう。

また、5月1日より U ブレンド豆以外のコーヒー豆が 1,000円 から 1,200円 に価格が変更になるそうです。欲しい方は今のうちに少し買い置きしておいても良いかもしれません。

f:id:iyemon018:20200427140810j:plain

こんなご時世ですが、自宅で楽しめそうなことはどんどんやっていっても良いかもしれません♪

良きコーヒーブレイクを!