From 13f0f7511ac3d338365cbf1a1ada9d22c6d3ba92 Mon Sep 17 00:00:00 2001 From: "hy.kim" Date: Fri, 8 Sep 2023 16:43:14 +0900 Subject: [PATCH] init --- durable_rules_2022254026_김홍열.ipynb | 555 ++ durable_rules_manual.html | 9318 ++++++++++++++++++++++ durable_rules_manual.ipynb | 1556 ++++ 3 files changed, 11429 insertions(+) create mode 100644 durable_rules_2022254026_김홍열.ipynb create mode 100644 durable_rules_manual.html create mode 100644 durable_rules_manual.ipynb diff --git a/durable_rules_2022254026_김홍열.ipynb b/durable_rules_2022254026_김홍열.ipynb new file mode 100644 index 0000000..5b6014c --- /dev/null +++ b/durable_rules_2022254026_김홍열.ipynb @@ -0,0 +1,555 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Durable Rules로 규칙 구현하기\n", + "- 산업인공지능학과 대학원\n", + " 2022254026\n", + " 김홍열" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 란?\n", + "* 비즈니스 룰 엔진을 구현하기 위한 라이브러리로 python, ruby, nodejs로 구현할 수 있다." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 설치하기 (Python)\n", + "``` plantext\n", + "pip install durable_rules" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 다운로드\n", + "[github - jruizgit/rules](https://github.com/jruizgit/rules)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 패키지 불러오기" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from durable.lang import *" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 구현할 규칙\n", + "![2차전지 라인 구성도](./images/2ndBatteryLine.png)\n", + "> 규칙\n", + "``` plain\n", + "1. IF Input Left_JR AND Right_JR\n", + "\t\tTHEN Form JR\n", + "\t\t\n", + "2. IF Form Left_JR AND Right_JR\n", + "\t\tTHEN Cut Left_JR AND Right_JR\n", + "\t\t\n", + "3. IF Cut Left_JR AND Right_JR\n", + "\t\tTHEN Align Left_JR AND Right_JR\n", + "\t\t\n", + "4. IF Pass Align Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\tAND F&C&T\n", + "\n", + "5. ELSE Align Vision_Inspection\n", + "\t\tTHEN Move Left Point\n", + "\t\tAND Move Right Point\n", + "\n", + "6. IF Move Left And Right Point\n", + "\t\tTHEN Align Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND F&C&T\n", + "\t\t\n", + "7. IF Pass F&C&T Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\tAND I&T\n", + "\n", + "8. ELSE F&C&T Vision_Inspection\n", + "\t\tTHEN Front Vision_Inspection\n", + "\t\tAND Rear Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\n", + "9. IF Front AND Rear Vision_Inspection\n", + "\t\tTHEN Rotate JR\n", + "\n", + "10. IF Rotate JR\n", + "\t\tTHEN Left Vision_Inspection\n", + "\t\tAND Right Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND I&T\n", + "\n", + "11. IF Pass I&T Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\tAND C&W\n", + "\t\t\n", + "12. ELSE I&T Vision_Inspection\n", + "\t\tTHEN Left Vision_Inspection\n", + "\t\tAND Right Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\t\n", + "13. IF Left AND Right Vision_Inspection\n", + "\t\tTHEN Move Up JR\n", + "\t\tAND Rotate JR\n", + "\t\t\n", + "14. IF Rotate JR\n", + "\t\tTHEN Front Vision_Inspection\n", + "\t\tAND Rear Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND C&W\n", + "\t\t\n", + "15. IF Pass C&W Vision_Inspection\n", + "\t\tTHEN Return None Result\n", + "\t\tAND R&T\n", + "\t\t\n", + "16. ELSE C&W Vision_Inspection\n", + "\t\tTHEN Left Vision_Inspection\n", + "\t\tAND Right Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND R&T\n", + "\t\t\n", + "17. IF Pass R&T Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\tAND Wrapping\n", + "\t\t\n", + "18. ELSE R&T Vision_Inspection\n", + "\t\tTHEN Left Vision_Inspection\n", + "\t\tAND Right Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND Wrapping\n", + "\t\t\n", + "19. IF Pass Wrapping Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\tAND C&C&W\n", + "\t\t\n", + "20. ELSE Wrapping Vision_Inspection\n", + "\t\tTHEN Front Vision_Inspection\n", + "\t\tAND Rear Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\t\n", + "21. IF Front AND Rear Vision_Inspection\n", + "\t\tTHEN Rotate JR\n", + "\t\t\n", + "22. IF Rotate JR\n", + "\t\tTHEN Left Vision_Inspection\n", + "\t\tAND Right Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\t\n", + "23. IF Left AND Right Vision_Inspection\n", + "\t\tTHEN Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND C&C&W\n", + "\t\t\n", + "24. IF Pass C&C&W Vision_Inspection\n", + "\t\tTHEN Return Result_None\n", + "\t\t\n", + "25. ELSE C&C&W Vision_Inspection\n", + "\t\tTHEN Long_Axis Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\n", + "26. IF Long_Axis Vision_Inspection\n", + "\t\tTHEN Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG\n", + "\t\tAND Rotate JR\n", + "\n", + "27. IF Rotate JR\n", + "\t\tTHEN Short_Axis Vision_Inspection\n", + "\t\tAND Retrun Result_OK\n", + "\t\t\tOR Retrun Result_NG" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. 각 라인 공정별 StateFlow 구현" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from durable.lang import *\n", + "\n", + "inspectionResult = ['OK','OK','OK','OK','OK','OK','OK'] #\n", + "def updateState(s, m): # \n", + " s.JobID = m.JobID\n", + " s.Pass = m.Pass\n", + " s.Left = m.Left\n", + " s.Right = m.Right\n", + " s.Judge0 = m.Judge0\n", + " s.Judge1 = m.Judge1\n", + " s.Judge2 = m.Judge2\n", + " s.Judge3 = m.Judge3\n", + " s.Judge4 = m.Judge4\n", + " s.Judge5 = m.Judge5\n", + " s.Judge6 = m.Judge6\n", + "\n", + "\n", + "with flowchart('battery_assembley_process'):\n", + " with stage('Input'):\n", + " @run\n", + " def nextToJR(c):\n", + " print('>> current stage is Input')\n", + " updateState(c.s, c.m)\n", + " print ('Input JobID: {0}'.format(c.m.JobID))\n", + "\n", + " to('JR').when_all((m.JobID != None))\n", + " to('Alarm').when_all((m.JobID == None))\n", + " \n", + " \n", + " with stage('JR'):\n", + " @run\n", + " def nextToF_C(c):\n", + " print('> current stage is JR')\n", + " updateState(c.s, c.m) # JobID는 전달되나 나머지 파라미터들은 전달되지 않음\n", + " print(c.s.Judge)\n", + " if c.m.Left is None: # @when_all(m.Left != None) 조건으로 함수를 별도로도 생성해보았으나 인식 안 됨\n", + " print('Alarm: Did not receive the Left JR.')\n", + " if c.m.Right is None:\n", + " print('Alarm: Did not receive the Right JR.')\n", + " if c.m.Left is not None and c.m.Right is not None:\n", + " print ('Both the Left JR[{0}] and the Right JR[{1}] have been assembled.'.format(c.m.Left, c.m.Right))\n", + " \n", + " to('Alarm').when_any((m.Left == None),\n", + " (m.Right == None))\n", + " to('F&C')\n", + "\n", + " with stage('F&C'):\n", + " @run\n", + " def NextToAlign(c):\n", + " print ('> current stage is F&C')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('F&C Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[0]}.')\n", + " else:\n", + " print('F&C Inspection is passed.')\n", + "\n", + " to('Align')\n", + "\n", + " with stage('Align'):\n", + " @run\n", + " def NextToI_T(c):\n", + " print ('> current stage is Align')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('Align Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[1]}.')\n", + " else:\n", + " print('Align Inspection is passed.')\n", + "\n", + " to('I&T')\n", + "\n", + " with stage('I&T'):\n", + " @run\n", + " def NextToCAPWelding(c):\n", + " print ('> current stage is I&T')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('I&T Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[2]}.')\n", + " else:\n", + " print('I&T Inspection is passed.')\n", + "\n", + " to('CAPWelding')\n", + "\n", + " with stage('CAPWelding'):\n", + " @run\n", + " def NextToRetainer(c):\n", + " print ('> current stage is CAPWelding')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('CAPWelding Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[3]}.')\n", + " else:\n", + " print('CAPWelding Inspection is passed.')\n", + "\n", + " to('Retainer')\n", + "\n", + " with stage('Retainer'):\n", + " @run\n", + " def NextToWrapping(c):\n", + " print ('> current stage is Retainer')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('Retainer Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[4]}.')\n", + " else:\n", + " print('Retainer Inspection is passed.')\n", + "\n", + " to('Wrapping')\n", + "\n", + " with stage('Wrapping'):\n", + " @run\n", + " def NextToC_CWelding(c):\n", + " print ('> current stage is Wrapping')\n", + " updateState(c.s, c.m)\n", + " if c.m.Pass is False:\n", + " print('Wrapping Inspection has been processed.')\n", + " print(f'F&C Judgement: {inspectionResult[5]}.')\n", + " else:\n", + " print('Wrapping Inspection is passed.')\n", + "\n", + " to('C&CWelding')\n", + "\n", + " with stage('C&CWelding'):\n", + " @run\n", + " def InspectionComplete(c):\n", + " print ('> current stage is C&CWelding')\n", + " updateState(c.s, c.m)\n", + " # c.s.JobID = c.m.JobID\n", + " # c.s.Pass = c.m.Pass\n", + " # c.s.Left = c.m.Left\n", + " # c.s.Right = c.m.Right\n", + " # c.s.Judge0 = c.m.Judge0\n", + " # c.s.Judge1 = c.m.Judge1\n", + " # c.s.Judge2 = c.m.Judge2\n", + " # c.s.Judge3 = c.m.Judge3\n", + " # c.s.Judge4 = c.m.Judge4\n", + " # c.s.Judge5 = c.m.Judge5\n", + " # c.s.Judge6 = c.m.Judge6\n", + " if c.m.Pass is False:\n", + " print('C&CWelding Inspection has been processed.')\n", + " # inspectionResult[6] = 'TEST'\n", + " # c.s.Judge6 = c.m.Judge6 = 'NG'\n", + " print(f'F&C Judgement: {inspectionResult[6]}.')\n", + " else:\n", + " print('C&CWelding Inspection is passed.')\n", + "\n", + " to('Output')\n", + "\n", + " with stage('Output'):\n", + " @run\n", + " def InspectionComplete(c):\n", + " print ('> current stage is Output')\n", + " result = 'OK'\n", + " for rst in inspectionResult:\n", + " if rst == 'NG':\n", + " result = 'NG'\n", + " break\n", + " # print(inspectionResult)\n", + " # print (result)\n", + " # print(c.s.JobID)\n", + " # print(c.s.Judge)\n", + " # print(c.s.Judge6)\n", + " print ('JobID[{0}]\\'s Result is {1}'.format(c.s.JobID, result))\n", + "\n", + " with stage('Alarm'):\n", + " @run\n", + " def Log(c):\n", + " print ('The process will not proceed due to an alarm being triggered. - [JobID: ' + str(c.m.JobID) + ']')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Input Data 구현" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ">> current stage is Input\n", + "The process will not proceed due to an alarm being triggered. - [JobID: None]\n", + ">> current stage is Input\n", + "> current stage is JR\n", + "> current stage is F&C\n", + "> current stage is Align\n", + "> current stage is I&T\n", + "> current stage is CAPWelding\n", + "> current stage is Retainer\n", + "> current stage is Wrapping\n", + "> current stage is C&CWelding\n", + "> current stage is Output\n", + "JobID[20230417]'s Result is OK\n", + ">> current stage is Input\n", + "> current stage is JR\n", + "> current stage is F&C\n", + "> current stage is Align\n", + "> current stage is I&T\n", + "> current stage is CAPWelding\n", + "> current stage is Retainer\n", + "> current stage is Wrapping\n", + "> current stage is C&CWelding\n", + "> current stage is Output\n", + "JobID[20230418]'s Result is OK\n", + ">> current stage is Input\n", + "> current stage is JR\n", + "> current stage is F&C\n", + "> current stage is Align\n", + "> current stage is I&T\n", + "> current stage is CAPWelding\n", + "> current stage is Retainer\n", + "> current stage is Wrapping\n", + "> current stage is C&CWelding\n", + "> current stage is Output\n", + "JobID[20230419]'s Result is NG\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '3',\n", + " 'id': 'sid-3',\n", + " '$s': 1,\n", + " 'running': True,\n", + " 'exception': 'exception caught \\'NoneType\\' object has no attribute \\'JobID\\', traceback [\\' File \"/config/.local/lib/python3.10/site-packages/durable/engine.py\", line 241, in run\\\\n self._func(c)\\\\n\\', \\' File \"/tmp/ipykernel_31962/4233634066.py\", line 129, in InspectionComplete\\\\n updateState(c.s, c.m)\\\\n\\', \\' File \"/tmp/ipykernel_31962/4233634066.py\", line 5, in updateState\\\\n s.JobID = m.JobID\\\\n\\']',\n", + " 'JobID': '20230419',\n", + " 'Pass': False,\n", + " 'Left': 100,\n", + " 'Right': 100}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 만들어진 규칙에 넣을 데이터들, 라인에 들어가는 각 배터리의 정보\n", + "post('battery_assembley_process', { 'JobID': None,\n", + " 'Pass': False,\n", + " 'Left': 100,\n", + " 'Right': 100,\n", + " # 'Judge0': None,\n", + " # 'Judge1': None,\n", + " # 'Judge2': None,\n", + " # 'Judge3': None,\n", + " # 'Judge4': None,\n", + " # 'Judge5': None,\n", + " # 'Judge6': None})\n", + " 'Judge':[None, None, None, None, None, None, None]})\n", + "post('battery_assembley_process', { 'sid': 1,\n", + " 'JobID': '20230417', \n", + " 'Pass': False,\n", + " 'Left': 100,\n", + " 'Right': None,\n", + " # 'Judge0': None,\n", + " # 'Judge1': None,\n", + " # 'Judge2': None,\n", + " # 'Judge3': None,\n", + " # 'Judge4': None,\n", + " # 'Judge5': None,\n", + " # 'Judge6': None})\n", + " 'Judge':[None, None, None, None, None, None, None]})\n", + "post('battery_assembley_process', { 'sid': 2,\n", + " 'JobID': '20230418', \n", + " 'Pass': False,\n", + " 'Left': None,\n", + " 'Right': 100,\n", + " # 'Judge0': None,\n", + " # 'Judge1': None,\n", + " # 'Judge2': None,\n", + " # 'Judge3': None,\n", + " # 'Judge4': None,\n", + " # 'Judge5': None,\n", + " # 'Judge6': None})\n", + " 'Judge':[None, None, None, None, None, None, None]})\n", + "\n", + "inspectionResult = ['OK','OK','OK','OK','NG','OK','OK'] # 내부적으로 Judege를 접근해서 변경할 수 없어서 테스트용으로 생성함\n", + "post('battery_assembley_process', { 'sid': 3,\n", + " 'JobID': '20230419', \n", + " 'Pass': False,\n", + " 'Left': 100,\n", + " 'Right': 100,\n", + " 'Judge0': None,\n", + " 'Judge1': None,\n", + " 'Judge2': None,\n", + " 'Judge3': None,\n", + " 'Judge4': None,\n", + " 'Judge5': None,\n", + " 'Judge6': 'OK'})\n", + " # 'Judge':[None, None, None, None, None, None, None]}) # Array를 제공하는듯 보이지만 allitems 같은 불편함을 감수해야됨\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 결과\n", + "* Durable_rule에서 사용되는 규칙을 별도로 습득 해야됨\n", + "* 내부적으로 메모리에 저장되는 것들을 접근하거나 디버깅할 수 없음\n", + "* 위 코드는 flowchart를 사용하였지만, statechart나 ruleset을 사용하여도 post를 상태마다 날려야 됨\n", + "* 내부적으로 어떻게 병렬처리 되는지 커스터마이징 할 수 없음\n", + "* 실무에 적용하기에 라이센스나 개발환경에 제약이 많음\n", + "* 상태패턴을 구현해서 직접 제어하는 것이 훨씬 효율적으로 보임" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/durable_rules_manual.html b/durable_rules_manual.html new file mode 100644 index 0000000..88ff393 --- /dev/null +++ b/durable_rules_manual.html @@ -0,0 +1,9318 @@ + + + + + +durable_rules_manual + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/durable_rules_manual.ipynb b/durable_rules_manual.ipynb new file mode 100644 index 0000000..37427d8 --- /dev/null +++ b/durable_rules_manual.ipynb @@ -0,0 +1,1556 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "# Durable Rules로 규칙 구현하기\n", + "- 산업인공지능학과 대학원\n", + " 2022254026\n", + " 김홍열" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 란?\n", + "* 비즈니스 룰 엔진을 구현하기 위한 라이브러리로 python, ruby, nodejs로 구현할 수 있다." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 설치하기 (Python)\n", + "``` plantext\n", + "pip install durable_rules" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Durable Rules 사용법\n", + "[github - jruizgit/rules](https://github.com/jruizgit/rules)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### 패키지 불러오기" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from durable.lang import *" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Rules (Trigger: @when_all(==, &, <<))\n", + "* 규칙은 프레임워크의 기본 구성 요소입니다.\n", + "* 규칙의 선행 조건은 규칙의 결과 조건(동작)을 실행하기 위해 충족되어야 하는 조건을 정의합니다.\n", + "* 관례적으로 m은 주어진 규칙에 의해 평가될 데이터를 나타냅니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with ruleset('test'):\n", + " # antecedent\n", + " @when_all(m.subject == 'World')\n", + " def say_hello(c):\n", + " # consequent\n", + " print ('Hello {0}'.format(c.m.subject))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hello World\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "post('test', { 'subject': 'World' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Facts (assert_fact)\n", + "* 사실은 지식 기반을 정의하는 데이터를 나타냅니다.\n", + "* 사실은 JSON 객체로 주장되며, 취소될 때까지 저장됩니다.\n", + "* 사실이 규칙의 선행 조건을 만족하면, 규칙의 결과 조건이 실행됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "with ruleset('animal'):\n", + " # will be triggered by 'Kermit eats flies'\n", + " @when_all((m.predicate == 'eats') & (m.object == 'flies'))\n", + " def frog(c):\n", + " c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'frog' })\n", + "\n", + " @when_all((m.predicate == 'eats') & (m.object == 'worms'))\n", + " def bird(c):\n", + " c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'bird' })\n", + "\n", + " # will be chained after asserting 'Kermit is frog'\n", + " @when_all((m.predicate == 'is') & (m.object == 'frog'))\n", + " def green(c):\n", + " c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'green' })\n", + "\n", + " @when_all((m.predicate == 'is') & (m.object == 'bird'))\n", + " def black(c):\n", + " c.assert_fact({ 'subject': c.m.subject, 'predicate': 'is', 'object': 'black' })\n", + "\n", + " @when_all(+m.subject)\n", + " def output(c):\n", + " print('Fact: {0} {1} {2}'.format(c.m.subject, c.m.predicate, c.m.object))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fact: Kermit is green\n", + "Fact: Kermit is frog\n", + "Fact: Kermit eats flies\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "assert_fact('animal', { 'subject': 'Kermit', 'predicate': 'eats', 'object': 'flies' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Events (post)\n", + "* 이벤트는 규칙에 전달되어 평가될 수 있습니다. \n", + "* 이벤트란 일시적인 사실로, 결과를 실행하기 직전에 취소되는 사실입니다.\n", + "* 따라서 이벤트는 한 번만 관찰할 수 있습니다.\n", + "* 이벤트는 관찰될 때까지 저장됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with ruleset('risk'):\n", + " @when_all(c.first << m.t == 'purchase',\n", + " c.second << m.location != c.first.location)\n", + " # the event pair will only be observed once\n", + " def fraud(c):\n", + " print('Fraud detected -> {0}, {1}'.format(c.first.location, c.second.location))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fraud detected -> CA, US\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "post('risk', {'t': 'purchase', 'location': 'US'})\n", + "post('risk', {'t': 'purchase', 'location': 'CA'})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### ✨위 예제에서 Event가 아닌 Fact를 적용하면 다음과 같이 출력됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fraud detected -> CA, US\n", + "Fraud detected -> US, CA\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "assert_fact('risk', {'t': 'purchase', 'location': 'US', 'last_location': None})\n", + "assert_fact('risk', {'t': 'purchase', 'location': 'CA', 'last_location': None})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "``` plaintext\n", + "Fraud detected -> US, CA\n", + "Fraud detected -> CA, US\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "예에서 두 가지 사실 모두 첫 번째 조건인 m.t == 'purchase'를 충족하며, 각 사실은 첫 번째 조건을 충족한 사실과 관련하여 두 번째 조건인 m.location != c.first.location을 충족합니다.\n", + "\n", + "이벤트는 일시적인 사실입니다. 사실이 발송될 예정이라면 즉시 취소됩니다. 위 예제에서 post를 사용할 때, 두 번째 쌍이 계산되는 시점에 이미 이벤트가 취소되어 있습니다.\n", + "\n", + "발송 전에 이벤트를 취소함으로써 작업 실행 중 계산해야 할 조합의 수를 줄일 수 있습니다." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### State (s, update_state)\n", + "* 규칙의 결과가 실행될 때 컨텍스트 상태를 사용할 수 있습니다. \n", + "* 동일한 컨텍스트 상태는 규칙 실행 간에 전달됩니다. \n", + "* 컨텍스트 상태는 삭제될 때까지 저장됩니다. \n", + "* 컨텍스트 상태 변경은 규칙에 의해 평가될 수 있습니다. \n", + "* 관례적으로 s는 규칙에 의해 평가되는 상태를 나타냅니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with ruleset('flow'):\n", + " # state condition uses 's'\n", + " @when_all(s.status == 'start')\n", + " def start(c):\n", + " # state update on 's'\n", + " c.s.status = 'next' \n", + " print('start')\n", + "\n", + " @when_all(s.status == 'next')\n", + " def next(c):\n", + " c.s.status = 'last' \n", + " print('next')\n", + "\n", + " @when_all(s.status == 'last')\n", + " def last(c):\n", + " c.s.status = 'end' \n", + " print('last')\n", + " # deletes state at the end\n", + " c.delete_state()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "start\n", + "next\n", + "last\n" + ] + } + ], + "source": [ + "update_state('flow', { 'status': 'start' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Identity (+속성, none(+속성))\n", + "* 같은 속성 이름과 값이 있는 팩트들은 단언(asserted)되거나 철회(retracted)될 때 동등하다고 간주됩니다.\n", + "* 같은 속성 이름과 값이 있는 이벤트들은 게시 시간이 중요하기 때문에 게시될 때 서로 다른 것으로 간주됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "with ruleset('bookstore'):\n", + " # this rule will trigger for events with status\n", + " @when_all(+m.status)\n", + " def event(c):\n", + " print('bookstore-> Reference {0} status {1}'.format(c.m.reference, c.m.status))\n", + "\n", + " @when_all(+m.name)\n", + " def fact(c):\n", + " print('bookstore-> Added {0}'.format(c.m.name))\n", + " \n", + " # this rule will be triggered when the fact is retracted\n", + " @when_all(none(+m.name))\n", + " def empty(c):\n", + " print('bookstore-> No books')\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bookstore-> Added The new book\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 단언(assert_fact)이 성공했기 때문에 예외를 발생시키지 않습니다. \n", + "assert_fact('bookstore', {\n", + " 'name': 'The new book',\n", + " 'seller': 'bookstore',\n", + " 'reference': '75323',\n", + " 'price': 500\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bookstore expected Message has already been observed: {\"reference\": \"75323\", \"name\": \"The new book\", \"price\": 500, \"seller\": \"bookstore\"}\n" + ] + } + ], + "source": [ + "# 이미 단언(assert_fact)된 사실이기 때문에 MessageObservedError가 발생합니다. \n", + "try:\n", + " assert_fact('bookstore', {\n", + " 'reference': '75323',\n", + " 'name': 'The new book',\n", + " 'price': 500,\n", + " 'seller': 'bookstore'\n", + " })\n", + "except BaseException as e:\n", + " print('bookstore expected {0}'.format(e.message))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bookstore-> Reference 75323 status Active\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 새로운 이벤트가 게시되기 때문에 예외를 발생시키지 않습니다. \n", + "post('bookstore', {\n", + " 'reference': '75323',\n", + " 'status': 'Active'\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bookstore-> Reference 75323 status Active\n" + ] + } + ], + "source": [ + "# 새로운 이벤트가 게시되기 때문에 예외를 발생시키지 않습니다.\n", + "post('bookstore', {\n", + " 'reference': '75323',\n", + " 'status': 'Active'\n", + "})\n", + "\n", + "retract_fact('bookstore', {\n", + " 'reference': '75323',\n", + " 'name': 'The new book',\n", + " 'price': 500,\n", + " 'seller': 'bookstore'\n", + "})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Pattern Matching" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### String Operations" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Correlated Sequence\n", + "* 규칙은 서로 관련된 이벤트 또는 사실의 시퀀스를 효율적으로 평가하는 데 사용할 수 있습니다. 아래 예시의 사기 탐지 규칙은 세 가지 이벤트 패턴을 보여줍니다: 두 번째 이벤트 금액이 첫 번째 이벤트 금액의 200%를 초과하고 세 번째 이벤트 금액이 다른 두 이벤트의 평균보다 큽니다.\n", + "* 기본적으로 관련된 시퀀스는 서로 다른 메시지를 캡처합니다. 아래 예시에서 두 번째 이벤트는 두 번째와 세 번째 조건을 모두 만족하지만, 이벤트는 두 번째 조건에 대해서만 캡처됩니다. distinct 속성을 사용하여 서로 다른 이벤트 또는 사실의 상관 관계를 비활성화할 수 있습니다.\n", + "* when_all 주석은 이벤트 또는 사실의 시퀀스를 표현합니다. << 연산자는 이후 표현식에서 참조할 수 있는 이벤트 또는 사실의 이름을 지정하는 데 사용됩니다. 이벤트 또는 사실을 참조할 때 모든 속성을 사용할 수 있습니다. 산술 연산자를 사용하여 복잡한 패턴을 표현할 수 있습니다.\n", + "* 산술 연산자: +, -, *, /" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fraud detected -> 50\n", + " -> 251\n", + " -> 200\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('risk'):\n", + " @when_all(# distinct(True),\n", + " c.first << m.amount > 10,\n", + " c.second << m.amount > c.first.amount * 2,\n", + " c.third << m.amount > (c.first.amount + c.second.amount) / 2)\n", + " def detected(c):\n", + " print('fraud detected -> {0}'.format(c.first.amount))\n", + " print(' -> {0}'.format(c.second.amount))\n", + " print(' -> {0}'.format(c.third.amount))\n", + " \n", + "post('risk', { 'amount': 50 })\n", + "post('risk', { 'amount': 200 })\n", + "post('risk', { 'amount': 251 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Choice of Sequences\n", + "* durable_rules는 보다 풍부한 이벤트 시퀀스를 표현하고 효율적으로 평가할 수 있게 해줍니다. 아래 예시에서 두 이벤트\\사실 시퀀스 각각이 동작을 실행합니다.\n", + "\n", + "* 다음 두 함수는 더 풍부한 이벤트 시퀀스를 정의하는 데 사용되고 결합할 수 있습니다:\n", + "\n", + "all: 이벤트 또는 사실 패턴의 집합입니다. 동작을 실행하려면 모든 패턴이 일치해야 합니다.\n", + "\n", + "any: 이벤트 또는 사실 패턴의 집합입니다. 어느 하나만 일치해도 동작이 실행됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Approved approve 1000\n", + "Approved jumbo 10000\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('expense'):\n", + " @when_any(all(c.first << m.subject == 'approve', \n", + " c.second << m.amount == 1000), \n", + " all(c.third << m.subject == 'jumbo', \n", + " c.fourth << m.amount == 10000))\n", + " def action(c):\n", + " if c.first:\n", + " print ('Approved {0} {1}'.format(c.first.subject, c.second.amount))\n", + " else:\n", + " print ('Approved {0} {1}'.format(c.third.subject, c.fourth.amount))\n", + " \n", + "\n", + "post('expense', { 'subject': 'approve' })\n", + "post('expense', { 'amount': 1000 })\n", + "post('expense', { 'subject': 'jumbo' })\n", + "post('expense', { 'amount': 10000 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Lack of Information\n", + "* 일부 경우에는 정보 부족이 중요한 의미를 가집니다. none 함수는 관련된 시퀀스가 있는 규칙에서 정보 부족을 평가하는 데 사용할 수 있습니다.\n", + "\n", + "* 참고: none 함수는 정보 부족에 대한 추론을 위해 정보가 필요합니다. 즉, 해당 규칙에 이벤트나 사실이 등록되지 않은 경우에는 동작을 실행하지 않습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fraud detected deposit withdrawal chargeback\n", + "fraud detected deposit withdrawal chargeback\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '1', 'id': 'sid-1', '$s': 1}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('risk'):\n", + " @when_all(c.first << m.t == 'deposit',\n", + " none(m.t == 'balance'),\n", + " c.third << m.t == 'withdrawal',\n", + " c.fourth << m.t == 'chargeback')\n", + " def detected(c):\n", + " print('fraud detected {0} {1} {2}'.format(c.first.t, c.third.t, c.fourth.t))\n", + " \n", + "assert_fact('risk', { 't': 'deposit' })\n", + "assert_fact('risk', { 't': 'withdrawal' })\n", + "assert_fact('risk', { 't': 'chargeback' })\n", + "\n", + "assert_fact('risk', { 'sid': 1, 't': 'balance' })\n", + "assert_fact('risk', { 'sid': 1, 't': 'deposit' })\n", + "assert_fact('risk', { 'sid': 1, 't': 'withdrawal' })\n", + "assert_fact('risk', { 'sid': 1, 't': 'chargeback' })\n", + "retract_fact('risk', { 'sid': 1, 't': 'balance' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Nested OBjects\n", + "* 중첩된 이벤트 또는 사실에 대한 질의도 지원됩니다.\n", + "* . 표기법은 중첩된 객체의 속성에 대한 조건을 정의하는 데 사용됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bill amount ->100\n", + "account payment amount ->100\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('expense'):\n", + " # use the '.' notation to match properties in nested objects\n", + " @when_all(c.bill << (m.t == 'bill') & (m.invoice.amount > 50),\n", + " c.account << (m.t == 'account') & (m.payment.invoice.amount == c.bill.invoice.amount))\n", + " def approved(c):\n", + " print ('bill amount ->{0}'.format(c.bill.invoice.amount))\n", + " print ('account payment amount ->{0}'.format(c.account.payment.invoice.amount))\n", + " \n", + "# one level of nesting\n", + "post('expense', {'t': 'bill', 'invoice': {'amount': 100}})\n", + " \n", + "#two levels of nesting\n", + "post('expense', {'t': 'account', 'payment': {'invoice': {'amount': 100}}})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Arrays" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "fraud 1 detected [150, 300, 450]\n", + "fraud 2 detected [{'amount': 200}, {'amount': 300}, {'amount': 450}]\n", + "fraud 3 detected ['one card', 'two cards', 'three cards']\n", + "fraud 4 detected [[10, 20, 30], [30, 40, 50], [10, 20]]\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('risk'):\n", + " # matching primitive array\n", + " @when_all(m.payments.allItems((item > 100) & (item < 500)))\n", + " def rule1(c):\n", + " print('fraud 1 detected {0}'.format(c.m.payments))\n", + "\n", + " # matching object array\n", + " @when_all(m.payments.allItems((item.amount < 250) | (item.amount >= 300)))\n", + " def rule2(c):\n", + " print('fraud 2 detected {0}'.format(c.m.payments))\n", + "\n", + " # pattern matching string array\n", + " @when_all(m.cards.anyItem(item.matches('three.*')))\n", + " def rule3(c):\n", + " print('fraud 3 detected {0}'.format(c.m.cards))\n", + "\n", + " # matching nested arrays\n", + " @when_all(m.payments.anyItem(item.allItems(item < 100)))\n", + " def rule4(c):\n", + " print('fraud 4 detected {0}'.format(c.m.payments))\n", + " \n", + "post('risk', {'payments': [ 150, 300, 450 ]})\n", + "post('risk', {'payments': [ { 'amount' : 200 }, { 'amount' : 300 }, { 'amount' : 450 } ]})\n", + "post('risk', {'cards': [ 'one card', 'two cards', 'three cards' ]})\n", + "post('risk', {'payments': [ [ 10, 20, 30 ], [ 30, 40, 50 ], [ 10, 20 ] ]}) " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Facts and Events as rvalues\n", + "* 스칼라 값(문자열, 숫자 및 부울 값) 외에도 표현식의 오른쪽에서 관찰된 사실이나 이벤트를 사용할 수 있습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "ename": "Exception", + "evalue": "Ruleset with name risk already registered", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[1;32mc:\\Users\\hykim\\Desktop\\04_durable_rules\\durable_rules_manual.ipynb Cell 42\u001b[0m line \u001b[0;36m1\n\u001b[0;32m 14\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m'\u001b[39m\u001b[39mfraud detected ->\u001b[39m\u001b[39m{0}\u001b[39;00m\u001b[39m'\u001b[39m\u001b[39m.\u001b[39mformat(c\u001b[39m.\u001b[39msecond\u001b[39m.\u001b[39mamount))\n\u001b[0;32m 16\u001b[0m \u001b[39m# post('risk', { 'debit': 220, 'credit': 100 })\u001b[39;00m\n\u001b[0;32m 17\u001b[0m \u001b[39m# post('risk', { 'debit': 150, 'credit': 100 })\u001b[39;00m\n\u001b[1;32m---> 18\u001b[0m post(\u001b[39m'\u001b[39;49m\u001b[39mrisk\u001b[39;49m\u001b[39m'\u001b[39;49m, { \u001b[39m'\u001b[39;49m\u001b[39mamount\u001b[39;49m\u001b[39m'\u001b[39;49m: \u001b[39m200\u001b[39;49m })\n\u001b[0;32m 19\u001b[0m post(\u001b[39m'\u001b[39m\u001b[39mrisk\u001b[39m\u001b[39m'\u001b[39m, { \u001b[39m'\u001b[39m\u001b[39mamount\u001b[39m\u001b[39m'\u001b[39m: \u001b[39m500\u001b[39m })\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\durable\\lang.py:670\u001b[0m, in \u001b[0;36mpost\u001b[1;34m(ruleset_name, message, complete)\u001b[0m\n\u001b[0;32m 669\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mpost\u001b[39m(ruleset_name, message, complete \u001b[39m=\u001b[39m \u001b[39mNone\u001b[39;00m):\n\u001b[1;32m--> 670\u001b[0m \u001b[39mreturn\u001b[39;00m get_host()\u001b[39m.\u001b[39mpost(ruleset_name, message, complete)\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\durable\\lang.py:663\u001b[0m, in \u001b[0;36mget_host\u001b[1;34m()\u001b[0m\n\u001b[0;32m 660\u001b[0m full_name, ruleset_definition \u001b[39m=\u001b[39m rset\u001b[39m.\u001b[39mdefine()\n\u001b[0;32m 661\u001b[0m ruleset_definitions[full_name] \u001b[39m=\u001b[39m ruleset_definition\n\u001b[1;32m--> 663\u001b[0m _main_host\u001b[39m.\u001b[39;49mregister_rulesets(ruleset_definitions)\n\u001b[0;32m 664\u001b[0m \u001b[39mfinally\u001b[39;00m:\n\u001b[0;32m 665\u001b[0m _rulesets \u001b[39m=\u001b[39m {}\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\durable\\engine.py:890\u001b[0m, in \u001b[0;36mHost.register_rulesets\u001b[1;34m(self, ruleset_definitions)\u001b[0m\n\u001b[0;32m 888\u001b[0m \u001b[39mfor\u001b[39;00m ruleset_name, ruleset \u001b[39min\u001b[39;00m rulesets\u001b[39m.\u001b[39mitems():\n\u001b[0;32m 889\u001b[0m \u001b[39mif\u001b[39;00m ruleset_name \u001b[39min\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_ruleset_directory:\n\u001b[1;32m--> 890\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mException\u001b[39;00m(\u001b[39m'\u001b[39m\u001b[39mRuleset with name \u001b[39m\u001b[39m{0}\u001b[39;00m\u001b[39m already registered\u001b[39m\u001b[39m'\u001b[39m\u001b[39m.\u001b[39mformat(ruleset_name))\n\u001b[0;32m 891\u001b[0m \u001b[39melse\u001b[39;00m: \n\u001b[0;32m 892\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_ruleset_directory[ruleset_name] \u001b[39m=\u001b[39m ruleset\n", + "\u001b[1;31mException\u001b[0m: Ruleset with name risk already registered" + ] + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('risk'):\n", + " # compares properties in the same event, this expression is evaluated in the client \n", + " @when_all(m.debit > m.credit * 2)\n", + " def fraud_1(c):\n", + " print('debit {0} more than twice the credit {1}'.format(c.m.debit, c.m.credit))\n", + "\n", + " # compares two correlated events, this expression is evaluated in the backend\n", + " @when_all(c.first << m.amount > 100,\n", + " c.second << m.amount > c.first.amount + m.amount / 2)\n", + " def fraud_2(c):\n", + " print('fraud detected ->{0}'.format(c.first.amount))\n", + " print('fraud detected ->{0}'.format(c.second.amount))\n", + " \n", + "post('risk', { 'debit': 220, 'credit': 100 })\n", + "post('risk', { 'debit': 150, 'credit': 100 })\n", + "post('risk', { 'amount': 200 })\n", + "post('risk', { 'amount': 500 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Consequents" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Conflict Resolution\n", + "* 이벤트와 사실 평가는 여러 결과를 초래할 수 있습니다. pri (중요도) 함수를 사용하여 트리거 순서를 제어할 수 있습니다. 낮은 값의 작업이 먼저 실행됩니다. 모든 작업의 기본값은 0입니다.\n", + "\n", + "* 이 예시에서, 마지막 규칙이 가장 높은 우선순위를 가지고 있으므로 먼저 트리거됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "attributes P1 ->50\n", + "attributes P2 ->50\n", + "attributes P3 ->50\n", + "attributes P2 ->150\n", + "attributes P3 ->150\n", + "attributes P3 ->250\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('attributes'):\n", + " @when_all(pri(3), m.amount < 300)\n", + " def first_detect(c):\n", + " print('attributes P3 ->{0}'.format(c.m.amount))\n", + " \n", + " @when_all(pri(2), m.amount < 200)\n", + " def second_detect(c):\n", + " print('attributes P2 ->{0}'.format(c.m.amount))\n", + " \n", + " @when_all(pri(1), m.amount < 100)\n", + " def third_detect(c):\n", + " print('attributes P1 ->{0}'.format(c.m.amount))\n", + " \n", + "assert_fact('attributes', { 'amount': 50 })\n", + "assert_fact('attributes', { 'amount': 150 })\n", + "assert_fact('attributes', { 'amount': 250 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Action Batches\n", + "* 많은 수의 이벤트 또는 사실이 결과를 만족시킬 때, 결과는 일괄적으로 전달될 수 있습니다.\n", + "\n", + "count: 동작을 예약하기 전에 규칙이 만족해야 하는 정확한 횟수를 정의합니다.\n", + "\n", + "cap: 동작을 예약하기 전에 규칙이 만족해야 하는 최대 횟수를 정의합니다.\n", + "\n", + "* 이 예시는 정확히 세 개의 승인을 일괄 처리하고 거절 수를 두 개로 제한합니다:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "approved [{'amount': 30}, {'amount': 20}, {'amount': 10}]\n", + "rejected [{'expense': {'amount': 400}, 'approval': {'review': True}}, {'expense': {'amount': 200}, 'approval': {'review': True}}]\n", + "rejected [{'expense': {'amount': 100}, 'approval': {'review': True}}]\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('expense'):\n", + " # this rule will trigger as soon as three events match the condition\n", + " @when_all(count(3), m.amount < 100)\n", + " def approve(c):\n", + " print('approved {0}'.format(c.m))\n", + "\n", + " # this rule will be triggered when 'expense' is asserted batching at most two results \n", + " @when_all(cap(2),\n", + " c.expense << m.amount >= 100,\n", + " c.approval << m.review == True)\n", + " def reject(c):\n", + " print('rejected {0}'.format(c.m))\n", + "\n", + "post_batch('expense', [{ 'amount': 10 },\n", + " { 'amount': 20 },\n", + " { 'amount': 100 },\n", + " { 'amount': 30 },\n", + " { 'amount': 200 },\n", + " { 'amount': 400 }])\n", + "assert_fact('expense', { 'review': True })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Async Actions\n", + "* 결과 동작은 비동기적일 수 있습니다. \n", + "* 동작이 완료되면 완료(complete) 함수를 호출해야 합니다. \n", + "* 기본적으로 동작은 5초 후에 포기된 것으로 간주됩니다. \n", + "* 이 값은 작업 함수에서 다른 숫자를 반환하거나 renew_action_lease를 호출함으로써 변경할 수 있습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "first completed\n", + "second completed\n" + ] + } + ], + "source": [ + "from durable.lang import *\n", + "import threading\n", + "\n", + "with ruleset('flow'):\n", + " timer = None\n", + "\n", + " def start_timer(time, callback):\n", + " timer = threading.Timer(time, callback)\n", + " timer.daemon = True \n", + " timer.start()\n", + "\n", + " @when_all(s.state == 'first')\n", + " # async actions take a callback argument to signal completion\n", + " def first(c, complete):\n", + " def end_first():\n", + " c.s.state = 'second' \n", + " print('first completed')\n", + "\n", + " # completes the action after 3 seconds\n", + " complete(None)\n", + " \n", + " start_timer(3, end_first)\n", + " \n", + " @when_all(s.state == 'second')\n", + " def second(c, complete):\n", + " def end_second():\n", + " c.s.state = 'third'\n", + " print('second completed')\n", + "\n", + " # completes the action after 6 seconds\n", + " # use the first argument to signal an error\n", + " complete(Exception('error detected'))\n", + "\n", + " start_timer(6, end_second)\n", + "\n", + " # overrides the 5 second default abandon timeout\n", + " return 10\n", + " \n", + "update_state('flow', { 'state': 'first' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Unhandled Exceptions\n", + "* 액션에서 예외가 처리되지 않은 경우, 예외는 컨텍스트 상태에 저장됩니다. \n", + "* 이를 통해 예외 처리 규칙을 작성할 수 있습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "exception caught Unhandled Exception!, traceback [' File \"/config/.local/lib/python3.10/site-packages/durable/engine.py\", line 241, in run\\n self._func(c)\\n', ' File \"/tmp/ipykernel_18661/3255112604.py\", line 7, in first\\n raise Exception(\\'Unhandled Exception!\\')\\n']\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('flow'):\n", + " \n", + " @when_all(m.action == 'start')\n", + " def first(c):\n", + " raise Exception('Unhandled Exception!')\n", + "\n", + " # when the exception property exists\n", + " @when_all(+s.exception)\n", + " def second(c):\n", + " print(c.s.exception)\n", + " c.s.exception = None\n", + " \n", + "post('flow', { 'action': 'start' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Flow Structures" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Statechart\n", + "* 규칙은 상태도(statecharts)를 사용하여 구성할 수 있습니다. 상태도는 결정적 유한 오토마타(DFA)입니다. 상태 컨텍스트는 가능한 여러 상태 중 하나에 있으며, 이러한 상태 간에 조건부 전환을 가집니다.\n", + "\n", + "* 상태도 규칙:\n", + "\n", + "1. 상태도는 하나 이상의 상태를 가질 수 있습니다.\n", + "2. 상태도에는 초기 상태가 필요합니다.\n", + "3. 초기 상태는 들어오는 간선이 없는 정점으로 정의됩니다.\n", + "4. 상태는 0개 이상의 트리거를 가질 수 있습니다.\n", + "5. 상태는 0개 이상의 상태를 가질 수 있습니다 (중첩 상태 참조).\n", + "6. 트리거에는 목적지 상태가 있습니다.\n", + "7. 트리거는 규칙을 가질 수 있습니다 (부재는 상태 진입을 의미).\n", + "8. 트리거는 액션을 가질 수 있습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "requesting approve amount 100\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with statechart('expense'):\n", + " # initial state 'input' with two triggers\n", + " with state('input'):\n", + " # trigger to move to 'denied' given a condition\n", + " @to('denied')\n", + " @when_all((m.subject == 'approve') & (m.amount > 1000))\n", + " # action executed before state change\n", + " def denied(c):\n", + " print ('denied amount {0}'.format(c.m.amount))\n", + " \n", + " @to('pending') \n", + " @when_all((m.subject == 'approve') & (m.amount <= 1000))\n", + " def request(c):\n", + " print ('requesting approve amount {0}'.format(c.m.amount))\n", + " \n", + " # intermediate state 'pending' with two triggers\n", + " with state('pending'):\n", + " @to('approved')\n", + " @when_all(m.subject == 'approved')\n", + " def approved(c):\n", + " print ('expense approved')\n", + " \n", + " @to('denied')\n", + " @when_all(m.subject == 'denied')\n", + " def denied(c):\n", + " print ('expense denied')\n", + " \n", + " # 'denied' and 'approved' are final states \n", + " state('denied')\n", + " state('approved')\n", + " \n", + "# events directed to default statechart instance\n", + "post('expense', { 'subject': 'approve', 'amount': 100 })\n", + "post('expense', { 'subject': 'approved' })\n", + "\n", + "# events directed to statechart instance with id '1'\n", + "post('expense', { 'sid': 1, 'subject': 'approve', 'amount': 100 })\n", + "post('expense', { 'sid': 1, 'subject': 'denied' })\n", + "\n", + "# events directed to statechart instance with id '2'\n", + "post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Nested States" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* 중첩 상태를 사용하면 컴팩트한 상태도를 작성할 수 있습니다. \n", + "* 컨텍스트가 중첩 상태에 있는 경우, 컨텍스트는 묵시적으로 주변 상태에도 있습니다. \n", + "* 상태도는 하위 상태 컨텍스트에서 모든 이벤트를 처리하려고 시도합니다. * 하위 상태가 이벤트를 처리하지 않으면, 이벤트는 자동으로 상위 상태 컨텍스트에서 처리됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "start process\n", + "continue processing\n", + "continue processing\n", + "cancel process\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with statechart('worker'):\n", + " # super-state 'work' has two states and one trigger\n", + " with state('work'):\n", + " # sub-state 'enter' has only one trigger\n", + " with state('enter'):\n", + " @to('process')\n", + " @when_all(m.subject == 'enter')\n", + " def continue_process(c):\n", + " print('start process')\n", + " \n", + " with state('process'):\n", + " @to('process')\n", + " @when_all(m.subject == 'continue')\n", + " def continue_process(c):\n", + " print('continue processing')\n", + "\n", + " # the super-state trigger will be evaluated for all sub-state triggers\n", + " @to('canceled')\n", + " @when_all(m.subject == 'cancel')\n", + " def cancel(c):\n", + " print('cancel process')\n", + "\n", + " state('canceled')\n", + "\n", + "# will move the statechart to the 'work.process' sub-state\n", + "post('worker', { 'subject': 'enter' })\n", + "\n", + "# will keep the statechart to the 'work.process' sub-state\n", + "post('worker', { 'subject': 'continue' })\n", + "post('worker', { 'subject': 'continue' })\n", + "\n", + "# will move the statechart out of the work state\n", + "post('worker', { 'subject': 'cancel' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Flowchart" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* 플로우차트는 규칙 세트 흐름을 구성하는 또 다른 방법입니다. 플로우차트에서 각 단계는 실행할 액션을 나타냅니다. 따라서 (상태도 상태와 달리) 컨텍스트 상태에 적용되면 다른 단계로 전환됩니다.\n", + "\n", + "* 플로우차트 규칙:\n", + "\n", + "1. 플로우차트는 하나 이상의 단계를 가질 수 있습니다.\n", + "2. 플로우차트에는 초기 단계가 필요합니다.\n", + "3. 초기 단계는 들어오는 간선이 없는 정점으로 정의됩니다.\n", + "4. 단계는 액션을 가질 수 있습니다.\n", + "5. 단계는 0개 이상의 조건을 가질 수 있습니다.\n", + "6. 조건에는 규칙과 목적지 단계가 있습니다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "requesting approve\n" + ] + }, + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1, 'running': True}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with flowchart('expense'):\n", + " # initial stage 'input' has two conditions\n", + " with stage('input'): \n", + " to('request').when_all((m.subject == 'approve') & (m.amount <= 1000))\n", + " to('deny').when_all((m.subject == 'approve') & (m.amount > 1000))\n", + " \n", + " # intermediate stage 'request' has an action and three conditions\n", + " with stage('request'):\n", + " @run\n", + " def request(c):\n", + " print('requesting approve')\n", + " \n", + " to('approve').when_all(m.subject == 'approved')\n", + " to('deny').when_all(m.subject == 'denied')\n", + " # reflexive condition: if met, returns to the same stage\n", + " to('request').when_all(m.subject == 'retry')\n", + " \n", + " with stage('approve'):\n", + " @run \n", + " def approved(c):\n", + " print('expense approved')\n", + "\n", + " with stage('deny'):\n", + " @run\n", + " def denied(c):\n", + " print('expense denied')\n", + "\n", + "# events for the default flowchart instance, approved after retry\n", + "post('expense', { 'subject': 'approve', 'amount': 100 })\n", + "# post('expense', { 'subject': 'retry' })\n", + "# post('expense', { 'subject': 'approved' })\n", + "\n", + "# # events for the flowchart instance '1', denied after first try\n", + "# post('expense', { 'sid': 1, 'subject': 'approve', 'amount': 100})\n", + "# post('expense', { 'sid': 1, 'subject': 'denied'})\n", + "\n", + "# # # event for the flowchart instance '2' immediately denied\n", + "# post('expense', { 'sid': 2, 'subject': 'approve', 'amount': 10000})" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Timer\n", + "* 이벤트는 타이머를 사용하여 예약할 수 있습니다. \n", + "* 시간 초과 조건은 규칙 전제에 포함될 수 있습니다.\n", + "* 기본적으로 타임아웃은 이벤트로 트리거됩니다 (한 번만 관찰됨).\n", + "* '수동 리셋' 타이머에 의해 타임아웃은 사실로도 트리거될 수 있으며, 액션 실행 중 타이머를 리셋할 수 있습니다 (마지막 예제 참조).\n", + "\n", + "start_timer: 지정된 이름과 지속 시간으로 타이머를 시작합니다 (manual_reset은 선택 사항입니다).\n", + "reset_timer: '수동 리셋' 타이머를 리셋합니다.\n", + "cancel_timer: 진행 중인 타이머를 취소합니다.\n", + "timeout: 전제 조건으로 사용됩니다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'sid': '0', 'id': 'sid-0', '$s': 1}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "timer timeout\n" + ] + } + ], + "source": [ + "from durable.lang import *\n", + "\n", + "with ruleset('timer'):\n", + " \n", + " @when_all(m.subject == 'start')\n", + " def start(c):\n", + " c.start_timer('MyTimer', 5)\n", + " \n", + " @when_all(timeout('MyTimer'))\n", + " def timer(c):\n", + " print('timer timeout')\n", + "\n", + "post('timer', { 'subject': 'start' })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* 아래 예제에서는 타이머를 사용하여 더 높은 이벤트 비율을 감지합니다." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from durable.lang import *\n", + "\n", + "with statechart('risk'):\n", + " with state('start'):\n", + " @to('meter')\n", + " def start(c):\n", + " c.start_timer('RiskTimer', 5)\n", + "\n", + " with state('meter'):\n", + " @to('fraud')\n", + " @when_all(count(3), c.message << m.amount > 100)\n", + " def fraud(c):\n", + " for e in c.m:\n", + " print(e.message) \n", + "\n", + " @to('exit')\n", + " @when_all(timeout('RiskTimer'))\n", + " def exit(c):\n", + " print('exit')\n", + "\n", + " state('fraud')\n", + " state('exit')\n", + "\n", + "# three events in a row will trigger the fraud rule\n", + "post('risk', { 'amount': 200 })\n", + "post('risk', { 'amount': 300 })\n", + "post('risk', { 'amount': 400 })\n", + "\n", + "# two events will exit after 5 seconds\n", + "post('risk', { 'sid': 1, 'amount': 500 })\n", + "post('risk', { 'sid': 1, 'amount': 600 })" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "* 이 예제에서는 속도를 측정하기 위해 수동 리셋 타이머를 사용합니다." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from durable.lang import *\n", + "\n", + "with statechart('risk'):\n", + " with state('start'):\n", + " @to('meter')\n", + " def start(c):\n", + " c.start_timer('VelocityTimer', 5, True)\n", + "\n", + " with state('meter'):\n", + " @to('meter')\n", + " @when_all(cap(5), \n", + " m.amount > 100,\n", + " timeout('VelocityTimer'))\n", + " def some_events(c):\n", + " print('velocity: {0} in 5 seconds'.format(len(c.m)))\n", + " # resets and restarts the manual reset timer\n", + " c.reset_timer('VelocityTimer')\n", + " c.start_timer('VelocityTimer', 5, True)\n", + "\n", + " @to('meter')\n", + " @when_all(pri(1), timeout('VelocityTimer'))\n", + " def no_events(c):\n", + " print('velocity: no events in 5 seconds')\n", + " c.reset_timer('VelocityTimer')\n", + " c.start_timer('VelocityTimer', 5, True)\n", + "\n", + "post('risk', { 'amount': 200 })\n", + "post('risk', { 'amount': 300 })\n", + "post('risk', { 'amount': 50 })\n", + "post('risk', { 'amount': 500 })\n", + "post('risk', { 'amount': 600 })" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +}