From ea9c650f82722ac2f09a19ed2a923569d9a6fd64 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Tue, 10 Sep 2024 10:08:12 +0200 Subject: [PATCH] Start with go version --- .gitignore | 6 +- .vscode/launch.json | 8 + src/__pycache__/__init__.cpython-311.pyc | Bin 171 -> 0 bytes src/__pycache__/config.cpython-311.pyc | Bin 2700 -> 0 bytes src/__pycache__/main.cpython-311.pyc | Bin 3909 -> 0 bytes src/components/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 182 -> 0 bytes .../__pycache__/poo_recorder.cpython-311.pyc | Bin 5644 -> 0 bytes src/components/poo_recorder.py | 78 ---- src/config.py | 40 -- src/go.mod | 11 + src/go.sum | 12 + src/helper/__init__.py | 0 src/helper/location_recorder/__init__.py | 0 .../google_location_reader.py | 61 --- .../location_recorder/gpx_location_reader.py | 44 --- src/helper/poo_recorder/__init__.py | 0 .../poo_recorder/old_poo_record_importer.py | 41 -- .../poo_recorder_helper/LICENSE} | 0 src/helper/poo_recorder_helper/cmd/reverse.go | 87 +++++ src/helper/poo_recorder_helper/cmd/root.go | 51 +++ src/helper/poo_recorder_helper/main.go | 11 + src/main.py | 60 --- src/tests/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 177 -> 0 bytes .../test_config.cpython-311-pytest-8.3.2.pyc | Bin 14184 -> 0 bytes .../test_main.cpython-311-pytest-8.3.2.pyc | Bin 178 -> 0 bytes src/tests/test_config.py | 70 ---- src/tests/test_main.py | 1 - src/util/__init__.py | 0 src/util/__pycache__/__init__.cpython-311.pyc | Bin 176 -> 0 bytes .../__pycache__/homeassistant.cpython-311.pyc | Bin 6300 -> 0 bytes .../location_recorder.cpython-311.pyc | Bin 11024 -> 0 bytes src/util/__pycache__/mqtt.cpython-311.pyc | Bin 5724 -> 0 bytes src/util/__pycache__/notion.cpython-311.pyc | Bin 6592 -> 0 bytes src/util/__pycache__/ticktick.cpython-311.pyc | Bin 6241 -> 0 bytes src/util/homeassistant.py | 71 ---- src/util/location_recorder.py | 120 ------ src/util/mqtt.py | 73 ---- src/util/notion.py | 82 ---- src/util/notion/notion.go | 62 +++ src/util/notion/notion_test.go | 27 ++ src/util/tests/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 182 -> 0 bytes ...tion_recorder.cpython-311-pytest-8.3.2.pyc | Bin 62534 -> 0 bytes src/util/tests/test_location_recorder.py | 354 ------------------ src/util/ticktick.py | 84 ----- 47 files changed, 272 insertions(+), 1182 deletions(-) delete mode 100644 src/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/__pycache__/config.cpython-311.pyc delete mode 100644 src/__pycache__/main.cpython-311.pyc delete mode 100644 src/components/__init__.py delete mode 100644 src/components/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/components/__pycache__/poo_recorder.cpython-311.pyc delete mode 100644 src/components/poo_recorder.py delete mode 100644 src/config.py create mode 100644 src/go.mod create mode 100644 src/go.sum delete mode 100644 src/helper/__init__.py delete mode 100644 src/helper/location_recorder/__init__.py delete mode 100644 src/helper/location_recorder/google_location_reader.py delete mode 100644 src/helper/location_recorder/gpx_location_reader.py delete mode 100644 src/helper/poo_recorder/__init__.py delete mode 100644 src/helper/poo_recorder/old_poo_record_importer.py rename src/{__init__.py => helper/poo_recorder_helper/LICENSE} (100%) create mode 100644 src/helper/poo_recorder_helper/cmd/reverse.go create mode 100644 src/helper/poo_recorder_helper/cmd/root.go create mode 100644 src/helper/poo_recorder_helper/main.go delete mode 100644 src/main.py delete mode 100644 src/tests/__init__.py delete mode 100644 src/tests/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/tests/__pycache__/test_config.cpython-311-pytest-8.3.2.pyc delete mode 100644 src/tests/__pycache__/test_main.cpython-311-pytest-8.3.2.pyc delete mode 100644 src/tests/test_config.py delete mode 100644 src/tests/test_main.py delete mode 100644 src/util/__init__.py delete mode 100644 src/util/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/util/__pycache__/homeassistant.cpython-311.pyc delete mode 100644 src/util/__pycache__/location_recorder.cpython-311.pyc delete mode 100644 src/util/__pycache__/mqtt.cpython-311.pyc delete mode 100644 src/util/__pycache__/notion.cpython-311.pyc delete mode 100644 src/util/__pycache__/ticktick.cpython-311.pyc delete mode 100644 src/util/homeassistant.py delete mode 100644 src/util/location_recorder.py delete mode 100644 src/util/mqtt.py delete mode 100644 src/util/notion.py create mode 100644 src/util/notion/notion.go create mode 100644 src/util/notion/notion_test.go delete mode 100644 src/util/tests/__init__.py delete mode 100644 src/util/tests/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/util/tests/__pycache__/test_location_recorder.cpython-311-pytest-8.3.2.pyc delete mode 100644 src/util/tests/test_location_recorder.py delete mode 100644 src/util/ticktick.py diff --git a/.gitignore b/.gitignore index b767a4d..5cc7ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,9 @@ go.work.sum # env file .env -**temp_data/** +temp_data/ # py file for branch switching *venv -*pytest_cache/** -*pycache*/** \ No newline at end of file +__pycache__/ +.pytest_cache/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 0f8103e..f6a584b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,14 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}" + }, + { + "name": "Launch Poo Reverse", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/src/helper/poo_recorder_helper/main.go", + "args": ["reverse"] } ] } \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 7d86c9198211ea7360556a53d2df6116e8983d26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171 zcmZ3^%ge<81kPrg(m?cM5CH>>P{wCAAY(d13PUi1CZpdPO4oIE6_NQg~j|p;sY}yBjX1K7*WIw6axU#Zz)^= diff --git a/src/__pycache__/config.cpython-311.pyc b/src/__pycache__/config.cpython-311.pyc deleted file mode 100644 index d1b8abfa70faef40071bb3ddbc3e7ec8258ae6c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2700 zcmcImO>7fK6rT0|c>N#Sl!B?iKq0unNt7bB2h@T>ND!4lBt_~{v|2n9Vq)*6v+J~p za0C_y|?3e z-@G^DpJK5vf)@Jqm)RdAgnnm(PJ=OP!q(p)*APZnD5Hv?2?EmtN$M6QcvsUmA~ zMbQ*Kmde43s;QNb7OI4`uz-k66?jK3M=DV*Dxd%&B8hDX2t9_c-)_Y_+Ipg+^*%RX z@VE%Q2jiW!f;5E*s8VF5tyxzPLQ0k55gEU%XJ5^wjaPVcS;?niIVkwj5$E>WfCX zN^CD^6GxvXOJ1mIwpBLmC5j0p_*|*zEK9@SU#bBkGcdJOu`uye@=eKhbZfrY8%uX< zz6W#-5u^ziX@SpC327o0h=c>D!9rwB!J-x0M7p?qCS5peI6qy7j%oX5Q&P}YEp-q>$- z`C3FYwYy~n?PtHj+cXqE?6gqG1vojJ3E=AhSbZ+&K2W{jO~`lPY&qVJsy)syzr!FP zut@yD@%tlgHYUVUH`C=9(1FF%|!aPLz$~5^ZF@v3mh=EuRNYrP}Y#sjQ}C1 z$M05GUqLIv2E6gOyUzl0*fzG??r-hLnC^TC>Fo+coDIr$D<@MG9zH4WGOe2^Koz5h zW{h5O*I1_Zs>`O*~K<14q}VA0Df#qYZV`RYxHqem>tg zHR)!i>Y1rVW~!4DoVmV|Vaw%|eGAHeqpbft{C@}w!D@CRj2;9-Z#IP~saT8sL~TRI znD;OUMpW{_@kZ*{x?NX?8tRa%4!xbG0u(u`b(&;?-5fLx0#LqX+vs}?ReI1Y^WC=Z z{uW1Lo~rAnQ6akSg>@Yc_G+2wQCpeFO^Er2EMxQ8L)i0l&nW8MO zyx`S|E7KP)6}+g;Zlq#`ICB=JtVZb}CXO)iK8Qz%rvgtlpJ#TSv2g9*K-79Z&C#*i zlq-$-y)KP4(}!xGHRI{p2DzTB`EG?AG zo0Od)RN;t!^Z3=uE0|@a7b#lhGAZ&4pRzwmY!7EkeBYD>gA&v6f|S@+d66(@cI)}~ zLykGij#Vwr`JA5wV0#~4si*w}_=J8026lMa|A1(Tf*>@}5x2i@qJ3_EZ=w&}-TEdv z<-WOZiUTbq2q&7-pxfU!<#O%#2OQqNH8NBN=TqEsE_8z}Fhd`sCUupQZ5zU25{Z?JN5{sla;LR0_% diff --git a/src/__pycache__/main.cpython-311.pyc b/src/__pycache__/main.cpython-311.pyc deleted file mode 100644 index 9665b7e1bb502f238fd0bd1aa277bb9eecaa914c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3909 zcma)8%WoUU8J{JWuccNbEmAM)K|O4jdXRSGHii+)k&_rnUCS>yEtmtb;*O-XmRxpr zr9_GkOk%WlV4%KP1p-tjS3z_z&`XZ_2MR2O1wBO;Xg!CK=3^I>0hfu0fhcdCyfW@?0FgYBJ>Q&Naj>jKGd5`KTdUIaJ zdsV*Z%lR13tNvmj7ht?k4HiSW5aa!-Pz>k7j1Q%A zpgB)&95ewuibwGH!Kvr)Wxj)UqDx^;?!&%Sj~suG-vlo`gW;M&`JjW%qU7FX$=z) z^S2~p9mc>lNjDa6eFSXymZsgo1&zqe!vCIDT2B z0w3=N-iEuNnL; z8Kf{wT1CR`Eu%jmK!PIXT16XX3+b0|T&wW4d|KQudCTZ7`j%Tt`$-=-*gp8^#2}|A zA1x3p88{E$1DDp661I8W(8_j9R`jx_w<*#b=?4#;o?z;&N6eg=|q$N}2Mu9m=c9DhB%!83V9kiu!lx{sX@X`bp^h z>&(|PkLRAunS6gY{U{`lgtBqBAW=%fPO$=`S zP?)a@Sxd;8{A=kYERm*Pu(A4`1lw*i9J6q)#-?uj2{txKsX*-;4~@&OpuOyc&WTHz zlRXLtiS1po{dXN#&&`c*5jhPuWSn-R(AmNoHu4TOIRgUP^cZx`fgSh6pUP%9Y4Spe${xnk2gthK4j-*oVNpX~H}}2b{ZxwTC@y$wQMqP{g_p#VPF} z2>`bvWug^8lyAs6LhG{=QO;HquKdoQtenh(8EtwNx|1-8jUB}@hq26yPt91S8oOe} zu9)F2;;%So63&)2E$fshTXe>}j15UqUws_EgEzhTalHO8IP?IrkI>5>id_$^qFE?u zpwQY3nKef@$1;nZ1EKq5-Fd3`HFS;S=syxh4~5a)VpW*41W1urP%S)Y@`KLC`yfZ1 z`kIBbT+bFX8FHNUvuJ#Y4`4maxO3+exLI za1tAP&(pn=-+<~OG@Zu!$$^TABXRanoZWx_#p$Ye(GoA3fljI;&No+!4wyqTW@Oe3 z&enSR%*4DIrlp4+^HO@w7EW$tRaqf4t7yI%>vTp>)efw}wJentLd&HRDPW5~RxErn2i2B_V1cHVx>7O6VVPJJOUn z*DHZPq{xmnrH#`gJygysCB?A4x8RXBCr#?`~LvkEzZXa z&^m@oTZI0*orgIIAO87iaKf$fDm-S&d5&C5!`U?tDy;W9GpzzwIOu!3NR@38c_ zreIZeGKPNOj_Uby@SUP2Z>o5K+yE`jG5wzqBXuvwaWyn!x~YZ+O*b8*r0J$(^p@$S z8X7g-R72-XH`UPFrkiSL%yd%?oi*K5LuX7k9iwo?|1kW8_)x5f#{0F@7WPYK@^UqK*-BnELkrc=f)!e*_-lc1#rR_D;Z}vO#ivc6`9(8+wHm)_#jlzu zR*Q~4756?f-&;1L_o~r*R`gy)tVM<&eZI%YuRu6BRG@_5Eew%KL}1;Lt770r@=1=O<}UiO^>!Tr_x*M2ox6-{brfN!VOsBLE`6-Zja%G!-Q(r5yM;P}VSkDZ TFJL1uYjqSN)v#@eS*7_uyPvPt diff --git a/src/components/__init__.py b/src/components/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/__pycache__/__init__.cpython-311.pyc b/src/components/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index cbe74a3b270fe532265455c8c3f2caaf8735612a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmZ3^%ge<81izFwrGe&ryk0@&FAkgB{FKt1RJ$TppvfS+i}``X2WCb_#t#fIqKFwN1^|?wE`R_4 diff --git a/src/components/__pycache__/poo_recorder.cpython-311.pyc b/src/components/__pycache__/poo_recorder.cpython-311.pyc deleted file mode 100644 index b1eccbfed61fdf78f7ad661b08f9d7a272a7004b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5644 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 diff --git a/src/components/poo_recorder.py b/src/components/poo_recorder.py deleted file mode 100644 index 058e875..0000000 --- a/src/components/poo_recorder.py +++ /dev/null @@ -1,78 +0,0 @@ -from datetime import datetime - -from pydantic import BaseModel - -from src.config import Config -from src.util.homeassistant import HomeAssistant -from src.util.mqtt import MQTT -from src.util.notion import NotionAsync - - -class PooRecorder: - CONFIG_TOPIC = "homeassistant/text/poo_recorder/config" - AVAILABILITY_TOPIC = "studiotj/poo_recorder/status" - COMMAND_TOPIC = "studiotj/poo_recorder/update_text" - STATE_TOPIC = "studiotj/poo_recorder/text" - JSON_TOPIC = "studiotj/poo_recorder/attributes" - ONLINE = "online" - OFFLINE = "offline" - - class RecordField(BaseModel): - status: str - latitude: str - longitude: str - - def __init__(self, mqtt: MQTT, notion: NotionAsync, homeassistant: HomeAssistant) -> None: - print("Poo Recorder Initialization...") - self._notion = notion - self._table_id = Config.get_env("POO_RECORD_NOTION_TABLE_ID") - self._mqtt = mqtt - self._mqtt.publish(PooRecorder.CONFIG_TOPIC, PooRecorder.compose_config(), retain=True) - self._mqtt.publish(PooRecorder.AVAILABILITY_TOPIC, PooRecorder.ONLINE, retain=True) - self._homeassistant = homeassistant - - async def _note(self, now: datetime, status: str, latitude: str, longitude: str) -> None: - formatted_date = now.strftime("%Y-%m-%d") - formatted_time = now.strftime("%H:%M") - status.strip() - await self._notion.append_table_row_text(self._table_id, [formatted_date, formatted_time, status, latitude + "," + longitude]) - - async def record(self, record_detail: RecordField) -> None: - webhook_id: str = Config.get_env("HOMEASSISTANT_POO_TRIGGER_ID") - self._publish_text(record_detail.status) - now = datetime.now(tz=datetime.now().astimezone().tzinfo) - self._publish_time(now) - await self._note(now, record_detail.status, record_detail.latitude, record_detail.longitude) - await self._homeassistant.trigger_webhook(payload={"status": record_detail.status}, webhook_id=webhook_id) - - def _publish_text(self, new_text: str) -> None: - self._mqtt.publish(PooRecorder.AVAILABILITY_TOPIC, PooRecorder.ONLINE, retain=True) - self._mqtt.publish(PooRecorder.STATE_TOPIC, new_text, retain=True) - - def _publish_time(self, time: datetime) -> None: - formatted_time = time.strftime("%a | %Y-%m-%d | %H:%M") - self._mqtt.publish(PooRecorder.AVAILABILITY_TOPIC, PooRecorder.ONLINE, retain=True) - json_string = {"last_poo": formatted_time} - self._mqtt.publish(PooRecorder.JSON_TOPIC, json_string, retain=True) - - @staticmethod - def compose_config() -> dict: - return { - "device": { - "name": "Dog Poop Recorder", - "model": "poop-recorder-backend", - "sw_version": Config.VERSION, - "identifiers": ["poo_recorder"], - "manufacturer": "Studio TJ", - }, - "unique_id": "poo_recorder", - "name": "Poo Status", - "availability_topic": PooRecorder.AVAILABILITY_TOPIC, - "availability_template": "{{ value_json.availability }}", - "json_attributes_topic": PooRecorder.JSON_TOPIC, - "min": 0, - "max": 255, - "mode": "text", - "command_topic": PooRecorder.COMMAND_TOPIC, - "state_topic": PooRecorder.STATE_TOPIC, - } diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 53ccb0c..0000000 --- a/src/config.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import TYPE_CHECKING, ClassVar - -from dotenv import dotenv_values, set_key, unset_key - -if TYPE_CHECKING: - from collections import OrderedDict - -config_path = Path(__file__).parent.resolve() -DOT_ENV_PATH = Path(config_path, ".env") -DOT_ENV_PATH.touch(mode=0o600, exist_ok=True) - - -class Config: - env_dict: ClassVar[OrderedDict[str, str]] = {} - dot_env_path = DOT_ENV_PATH - VERSION = "2.0" - - @staticmethod - def init(dotenv_path: str = DOT_ENV_PATH) -> None: - Config.dot_env_path = dotenv_path - Config.env_dict = dotenv_values(dotenv_path=dotenv_path) - - @staticmethod - def get_env(key: str) -> str | None: - if key in Config.env_dict: - return Config.env_dict[key] - return None - - @staticmethod - def update_env(key: str, value: str) -> None: - set_key(Config.dot_env_path, key, value) - Config.env_dict = dotenv_values(dotenv_path=Config.dot_env_path) - - @staticmethod - def remove_env(key: str) -> None: - unset_key(Config.dot_env_path, key) - Config.env_dict = dotenv_values(dotenv_path=Config.dot_env_path) diff --git a/src/go.mod b/src/go.mod new file mode 100644 index 0000000..4baf34f --- /dev/null +++ b/src/go.mod @@ -0,0 +1,11 @@ +module github.com/t-liu93/home-automation-backend + +go 1.23.0 + +require github.com/jomei/notionapi v1.13.2 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/src/go.sum b/src/go.sum new file mode 100644 index 0000000..775143e --- /dev/null +++ b/src/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jomei/notionapi v1.13.2 h1:YpHKNpkoTMlUfWTlVIodOmQDgRKjfwmtSNVa6/6yC9E= +github.com/jomei/notionapi v1.13.2/go.mod h1:BqzP6JBddpBnXvMSIxiR5dCoCjKngmz5QNl1ONDlDoM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/helper/__init__.py b/src/helper/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/helper/location_recorder/__init__.py b/src/helper/location_recorder/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/helper/location_recorder/google_location_reader.py b/src/helper/location_recorder/google_location_reader.py deleted file mode 100644 index befbe17..0000000 --- a/src/helper/location_recorder/google_location_reader.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse -import asyncio -import json -import sys -from datetime import datetime -from pathlib import Path - -current_file_path = Path(__file__).resolve().parent -sys.path.append(str(current_file_path / ".." / ".." / "..")) -from src.util.location_recorder import LocationData, LocationRecorder # noqa: E402 - -# Create an argument parser -parser = argparse.ArgumentParser(description="Google Location Reader") - -# Add an argument for the JSON file path -parser.add_argument("--json-file", type=str, help="Path to the JSON file") - -# Parse the command-line arguments -args = parser.parse_args() - -json_file_path: str = args.json_file - -db_path = current_file_path / ".." / ".." / ".." / "temp_data" / "test.db" -location_recorder = LocationRecorder(db_path=str(db_path)) - -# Open the JSON file -with Path.open(json_file_path) as json_file: - data = json.load(json_file) - - -locations: list[dict] = data["locations"] -print(type(locations), len(locations)) - - -async def insert() -> None: - nr_waypoints = 0 - await location_recorder.create_db_engine() - locations_dict: dict[datetime, LocationData] = {} - for location in locations: - nr_waypoints += 1 - try: - latitude: float = location["latitudeE7"] / 1e7 - longitude: float = location["longitudeE7"] / 1e7 - except KeyError: - continue - altitude: float = location.get("altitude", None) - try: - date_time = datetime.strptime(location["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z") - except ValueError: - date_time = datetime.strptime(location["timestamp"], "%Y-%m-%dT%H:%M:%S%z") - locations_dict[date_time] = LocationData( - latitude=latitude, - longitude=longitude, - altitude=altitude, - ) - await location_recorder.insert_locations("Tianyu", locations=locations_dict) - print(nr_waypoints) - await location_recorder.dispose_db_engine() - - -asyncio.run(insert()) diff --git a/src/helper/location_recorder/gpx_location_reader.py b/src/helper/location_recorder/gpx_location_reader.py deleted file mode 100644 index e8f0277..0000000 --- a/src/helper/location_recorder/gpx_location_reader.py +++ /dev/null @@ -1,44 +0,0 @@ -import argparse -import asyncio -import sys -from datetime import UTC -from pathlib import Path - -import gpxpy -import gpxpy.gpx - -current_file_path = Path(__file__).resolve().parent -sys.path.append(str(current_file_path / ".." / ".." / "..")) -from src.util.location_recorder import LocationData, LocationRecorder # noqa: E402 - -parser = argparse.ArgumentParser(description="GPX Location Reader") - -parser.add_argument("--gpx-file", type=str, help="Path to the GPX file") - -args = parser.parse_args() - -gpx_location = args.gpx_file - -gpx_file = Path.open(gpx_location) -gpx = gpxpy.parse(gpx_file) - -db_path = current_file_path / ".." / ".." / ".." / "temp_data" / "test.db" -location_recorder = LocationRecorder(db_path=str(db_path)) - - -async def iterate_and_insert() -> None: - nr_waypoints = 0 - await location_recorder.create_db_engine() - for track in gpx.tracks: - for segment in track.segments: - for point in segment.points: - nr_waypoints += 1 - print(f"Point at ({point.latitude},{point.longitude}) -> {point.time}") - point.time = point.time.replace(tzinfo=UTC) - location_data = LocationData(latitude=point.latitude, longitude=point.longitude, altitude=point.elevation) - await location_recorder.insert_location(person="Tianyu", date_time=point.time, location=location_data) - await location_recorder.dispose_db_engine() - print(nr_waypoints) - - -asyncio.run(iterate_and_insert()) diff --git a/src/helper/poo_recorder/__init__.py b/src/helper/poo_recorder/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/helper/poo_recorder/old_poo_record_importer.py b/src/helper/poo_recorder/old_poo_record_importer.py deleted file mode 100644 index f36dc57..0000000 --- a/src/helper/poo_recorder/old_poo_record_importer.py +++ /dev/null @@ -1,41 +0,0 @@ -import asyncio -import datetime -from pathlib import Path - -from src.config import Config -from src.util.notion import NotionAsync - -Config.init() - -notion = NotionAsync(token=Config.get_env("NOTION_TOKEN")) - -current_file_path = Path(__file__).resolve() - -current_dir = str(current_file_path.parent) - - -rows: list[str] = [] - - -async def update_rows() -> None: - header: dict = await notion.get_block_children(block_id=Config.get_env("POO_RECORD_NOTION_TABLE_ID"), page_size=1) - header_id = header["results"][0]["id"] - with Path.open(current_dir + "/../../../temp_data/old_poo_record.txt") as file: - content = file.read() - rows = content.split("\n") - rows.reverse() - - for row in rows: - t = row[0:5] - date = row[8:19] - formatted_date = datetime.datetime.strptime(date, "%a, %d %b").astimezone().replace(year=2024).strftime("%Y-%m-%d") - status = row[20:] - print(f"{formatted_date} {t} {status}") - await notion.append_table_row_text( - table_id=Config.get_env("POO_RECORD_NOTION_TABLE_ID"), - text_list=[formatted_date, t, status, "0,0"], - after=header_id, - ) - - -asyncio.run(update_rows()) diff --git a/src/__init__.py b/src/helper/poo_recorder_helper/LICENSE similarity index 100% rename from src/__init__.py rename to src/helper/poo_recorder_helper/LICENSE diff --git a/src/helper/poo_recorder_helper/cmd/reverse.go b/src/helper/poo_recorder_helper/cmd/reverse.go new file mode 100644 index 0000000..58eafc0 --- /dev/null +++ b/src/helper/poo_recorder_helper/cmd/reverse.go @@ -0,0 +1,87 @@ +/* +Copyright © 2024 Tianyu Liu +*/ +package cmd + +import ( + "context" + "fmt" + "time" + + "github.com/jomei/notionapi" + "github.com/spf13/cobra" + "github.com/t-liu93/home-automation-backend/util/notion" +) + +var notionToken string +var notionTableId string + +// reverseCmd represents the reverse command +var reverseCmd = &cobra.Command{ + Use: "reverse", + Short: "Reverse given poo recording table", + Run: reverseRun, +} + +func reverseRun(cmd *cobra.Command, args []string) { + rows := []notionapi.Block{} + fmt.Println("Reverse table ID: ", notionTableId) + notion.Init(notionToken) + headerBlock := <-notion.AGetBlockChildren(notionTableId, "", 100) + headerId := headerBlock.Children[0].GetID() + nextCursor := headerId.String() + hasMore := true + for hasMore { + blockChildren := <-notion.AGetBlockChildren(notionTableId, nextCursor, 100) + rows = append(rows, blockChildren.Children...) + hasMore = blockChildren.HasMore + nextCursor = blockChildren.NextCursor + } + rows = rows[1:] + rowsR := reverseTable(rows) + for _, row := range rowsR { + notion.GetClient().Block.Delete(context.Background(), row.GetID()) + fmt.Println("Deleted row: ", row.GetID()) + time.Sleep(400 * time.Millisecond) + } + after := headerId + for len(rowsR) > 0 { + var rowsToWrite []notionapi.Block + if len(rowsR) > 100 { + rowsToWrite = rowsR[:100] + } else { + rowsToWrite = rowsR + } + res, err := notion.GetClient().Block.AppendChildren(context.Background(), notionapi.BlockID(notionTableId), ¬ionapi.AppendBlockChildrenRequest{ + After: after, + Children: rowsToWrite, + }) + after = rowsToWrite[len(rowsToWrite)-1].GetID() + rowsR = rowsR[len(rowsToWrite):] + fmt.Println(res, err) + } + +} + +func reverseTable[T any](rows []T) []T { + for i, j := 0, len(rows)-1; i < j; i, j = i+1, j-1 { + rows[i], rows[j] = rows[j], rows[i] + } + return rows +} + +func init() { + rootCmd.AddCommand(reverseCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // reverseCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // reverseCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + reverseCmd.Flags().StringVar(¬ionToken, "token", "", "Notion API token") + reverseCmd.Flags().StringVar(¬ionTableId, "table-id", "", "Notion table id to reverse") +} diff --git a/src/helper/poo_recorder_helper/cmd/root.go b/src/helper/poo_recorder_helper/cmd/root.go new file mode 100644 index 0000000..7f02bb8 --- /dev/null +++ b/src/helper/poo_recorder_helper/cmd/root.go @@ -0,0 +1,51 @@ +/* +Copyright © 2024 Tianyu Liu + +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + + + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "poo_recorder_helper", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.poo_recorder_helper.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + + diff --git a/src/helper/poo_recorder_helper/main.go b/src/helper/poo_recorder_helper/main.go new file mode 100644 index 0000000..14e2910 --- /dev/null +++ b/src/helper/poo_recorder_helper/main.go @@ -0,0 +1,11 @@ +/* +Copyright © 2024 Tianyu Liu + +*/ +package main + +import "github.com/t-liu93/home-automation-backend/helper/poo_recorder_helper/cmd" + +func main() { + cmd.Execute() +} diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 9eadbc8..0000000 --- a/src/main.py +++ /dev/null @@ -1,60 +0,0 @@ -from contextlib import asynccontextmanager -from pathlib import Path - -from fastapi import FastAPI - -from src.components.poo_recorder import PooRecorder -from src.config import Config -from src.util.homeassistant import HomeAssistant -from src.util.location_recorder import LocationRecorder -from src.util.mqtt import MQTT -from src.util.notion import NotionAsync -from src.util.ticktick import TickTick - -Config.init() - -location_recorder_db = str(Path(__file__).resolve().parent / ".." / "location_recorder.db") - -ticktick = TickTick() -notion = NotionAsync(token=Config.get_env(key="NOTION_TOKEN")) -mqtt = MQTT() -location_recorder = LocationRecorder(db_path=location_recorder_db) -homeassistant = HomeAssistant(ticktick=ticktick, location_recorder=location_recorder) -poo_recorder = PooRecorder(mqtt=mqtt, notion=notion, homeassistant=homeassistant) - - -@asynccontextmanager -async def _lifespan(_app: FastAPI): # noqa: ANN202 - await mqtt.start() - await location_recorder.create_db_engine() - yield - await mqtt.stop() - await location_recorder.dispose_db_engine() - - -app = FastAPI(lifespan=_lifespan) - - -@app.get("/homeassistant/status") -async def get_status() -> dict: - return {"Status": "Ok"} - - -@app.post("/homeassistant/publish") -async def homeassistant_publish(payload: HomeAssistant.Message) -> dict: - return await homeassistant.process_message(message=payload) - - -# Poo recorder -@app.post("/poo/record") -async def record(record_detail: PooRecorder.RecordField) -> PooRecorder.RecordField: - await poo_recorder.record(record_detail) - return record_detail - - -# ticktick -@app.get("/ticktick/auth/code") -async def ticktick_auth(code: str, state: str) -> dict: - if await ticktick.retrieve_access_token(code, state): - return {"State": "Token Retrieved"} - return {"State": "Token Retrieval Failed"} diff --git a/src/tests/__init__.py b/src/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/tests/__pycache__/__init__.cpython-311.pyc b/src/tests/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index b9de1f88b59875e64a915ef51d4ceac9ad2dff76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177 zcmZ3^%ge<81h+rzO#{)7K?DpiLK&agfQ;!3DGb33nv8xc8H$*I{LdiCUoQF?`MIh3 zC7FqNm8JUS`9;~q1&PV2U{T$~(vtk##FEVXJl&+k0P}P$ApigX diff --git a/src/tests/__pycache__/test_config.cpython-311-pytest-8.3.2.pyc b/src/tests/__pycache__/test_config.cpython-311-pytest-8.3.2.pyc deleted file mode 100644 index 6a03a57e8d1507f608dc0ba2fd05594c90cbfeb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14184 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 diff --git a/src/tests/__pycache__/test_main.cpython-311-pytest-8.3.2.pyc b/src/tests/__pycache__/test_main.cpython-311-pytest-8.3.2.pyc deleted file mode 100644 index f72a1f0c4fe23580a56b7d01e3028fe98300b419..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmZ3^%ge<81Y$qVr7;5O#~=<2fCNC`GaHbY&XB?o%%I8Wx00cV2_y)T`Q@sgk)NBY zUy_-aS6QlGo?nz*T#%TY3KrE(EG@~;O)SaG&(lpxOwLZtOVKYbO4ct)EiNerlkvHU qnR$8zmA^P_a`RJ4b5iY!Sb?U3>@DU85+9fu85ut?z=$Gdpcnwcq%8{o diff --git a/src/tests/test_config.py b/src/tests/test_config.py deleted file mode 100644 index 02bb2a8..0000000 --- a/src/tests/test_config.py +++ /dev/null @@ -1,70 +0,0 @@ -from collections import OrderedDict -from pathlib import Path - -import pytest -from dotenv import dotenv_values, set_key - -from src.config import Config - -CONFIG_PATH = Path(__file__).parent.resolve() -TEST_DOT_ENV_PATH = Path(CONFIG_PATH, ".env_test") - -EXPECTED_ENV_DICT: OrderedDict[str, str] = OrderedDict( - { - "KEY_1": "VALUE_1", - "KEY_2": "VALUE_2", - "NOTION_TOKEN": "1234454_234324", - }, -) - - -@pytest.fixture -def _prepare_test_dot_env() -> any: - TEST_DOT_ENV_PATH.touch(mode=0o600, exist_ok=True) - for key, value in EXPECTED_ENV_DICT.items(): - set_key(TEST_DOT_ENV_PATH, key, value) - yield - TEST_DOT_ENV_PATH.unlink() - - -@pytest.fixture -def _load_test_dot_env(_prepare_test_dot_env: any) -> None: - Config.init(dotenv_path=TEST_DOT_ENV_PATH) - - -@pytest.mark.usefixtures("_prepare_test_dot_env") -def test_init_config() -> None: - assert Config.env_dict == {} - Config.init(dotenv_path=TEST_DOT_ENV_PATH) - assert Config.env_dict == EXPECTED_ENV_DICT - dict_from_file = dotenv_values(dotenv_path=TEST_DOT_ENV_PATH) - assert dict_from_file == EXPECTED_ENV_DICT - - -@pytest.mark.usefixtures("_load_test_dot_env") -def test_get_config() -> None: - assert Config.get_env("NON_EXISTING_KEY") is None - key_1 = "KEY_1" - assert Config.get_env(key_1) == EXPECTED_ENV_DICT[key_1] - - -@pytest.mark.usefixtures("_load_test_dot_env") -def test_update_config() -> None: - key = "KEY_1" - value = EXPECTED_ENV_DICT[key] - new_value = "NEW_VALUE" - assert Config.get_env(key) == value - Config.update_env(key, new_value) - assert Config.get_env(key) == new_value - dict_from_file = dotenv_values(dotenv_path=TEST_DOT_ENV_PATH) - assert dict_from_file[key] == new_value - - -@pytest.mark.usefixtures("_load_test_dot_env") -def test_remove_config() -> None: - key = "KEY_1" - assert Config.get_env(key) == EXPECTED_ENV_DICT[key] - Config.remove_env(key) - assert Config.get_env(key) is None - dict_from_file = dotenv_values(dotenv_path=TEST_DOT_ENV_PATH) - assert key not in dict_from_file diff --git a/src/tests/test_main.py b/src/tests/test_main.py deleted file mode 100644 index 8b13789..0000000 --- a/src/tests/test_main.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/util/__init__.py b/src/util/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/util/__pycache__/__init__.cpython-311.pyc b/src/util/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index bb2a040f0aa0687493f2b342d79584888c9f601c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 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 S-2v;_N*?33r`s^d8vhTP@YWOn diff --git a/src/util/__pycache__/location_recorder.cpython-311.pyc b/src/util/__pycache__/location_recorder.cpython-311.pyc deleted file mode 100644 index c86cd64e493f821cb8be63e7e4628f9e10e7a8b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11024 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 diff --git a/src/util/__pycache__/mqtt.cpython-311.pyc b/src/util/__pycache__/mqtt.cpython-311.pyc deleted file mode 100644 index a8566032db894537bc54bb4a334e567af885364c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5724 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 boQ}FV9aZHi?hR0rt?AdS|C?<_nK1tixfIo5 diff --git a/src/util/__pycache__/notion.cpython-311.pyc b/src/util/__pycache__/notion.cpython-311.pyc deleted file mode 100644 index 8b26df85828b7bae0fdf5757bed3064f3d37538a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6592 zcmb_gO>7&-72aQR`6KGbB4z#1(T^RAt}WTF?Kn*wTeh6Uh)blgleA3S&5E1vyqu9)otO{I2Qv!K>5{;`!4c^ON94N#$9)ZdH{FHD*Yy`J6RL3r zfHSx)XSl|x0B1-K-xWa){N25q3-QPDCpdil%1fiUlJj zIswffxvc4gh-PZpyk;1VJe$+=1X-tyYlZBYd`>Tz+1jw|QJVz+WB^`ob2^vd36~Mx z1$F5n5nhE_BEq1OBVE*gVLGB=QYV-#7EF+ohH6Ds3);M{s!mu{=Zj=9kL9SU{%ldp z*IGJN)iYI9z0Wusa(m+Nc z66iy2=p*1L@8q&`s1u4HbR_fIf}VaB2K@||hSSA@?nrY~pH0a$isN=5i6QAk(ubrA z37b_9mU`<1d5!%*{A=kbpDUmHa9Kh=_|4 zI9*1>KalV8fKB+jDH9+8;NjWAg8&4@iPjg+03n%m70@_zX&D;;#y=T_mfM`!kP4vU z9|-=DK(?560M`^;86V8Feic1aUd?OsGemoBDej%cM7=rzT@BP|nfs%;dfJ*iS(!X( zPo6BhdInGEB>Jqjm^al~Eo&C3i=cEW*lZ}_UcNY znG$ZUNQr89;#S<+dlGZ^sY>@LyZaP!oN{xeUT_VGa$A%Mb^H?obkmSz7m;sBV9H%2 z08fd$Ktk}8$uJoqT{mPf;3(;anjje?JK(91-6Rgr5ZOa^!ZS=>BzwsKa7LgsPLfcH zk}#Mf;KnN_sAgHTxT6s1l#HYSdJ-i_|DmiZRCtEohf@)yOUBj+lczR)WfULL@MqeFTN=bathovlVosmQ>

Jc$JXM!bXrwy!4op65*#v zHaMwkTM`Pl_g1Fi_h7*7H2hsqiHAe*(8D0Q=Ao`414Pg3rcPhNm3pI5)NQC?(dVJw zmhWLdv{6k%v7VcGJ2tW&8()i!e=}~y#w)Q`?AR-ovSntPaHN@HG2h-xz4};zp<2`S z9(i3T2}HoX$W27N$}oy2!ZiIpRI*(k_6BVxufwK&9I8M|4M;h%>%WNS2ApEG0Ar}E z+tPp+O8&M9=;u|^(D3ilzfFJh=ACq9=!89V!b&vfWx3kfd+WWM*H?mToqH{1ud5`5 z4Fy51y`l;*&i@-Yt;*X}q(_0T!_ZE|o!-YeV$M;FQFSGrgEB=H!>7Vd^hi&ir9rO_ zn;H?i#%Ulmq$LKv$lHn3dgAa};_&L5R^o6aan??pwYr+KtIYRr{_4)W-MQaV_Pfdq z;h|5(@bN5!^aD8cUdrMG>!}g*?ml^P=UAY1hS)NW@Lm5y?}Q zH_O+h64!ojW^q$`5ZF3yx;~x?rXS%3mJ-|mim}rPQr&>_qCpY(j*uf@8D^9p;=8QP zLI{8kr|>PVEiAxArDhmf{8&Xr!4-h{!)C9*oURd_IwAK7s5DSv;2`ygJK9!~%0^S& zUP}{RlRuyT7>rjnZZroQqy&&`2gxaj$Up$p7PwyDZrF>FK>tjOi8d83(v)r z2uE6WeW>Bak+XU}Z!k5aWH{_G$WR=FT_)JQ$LVUeXDZMHA|`Bdftbo%r(! zYh-nL_JTgsN%1qZI@ zS&YS$`UUTj4=jzqLcvp?7g+q}^#&C~z)gMYfYA&TZ(o3>|8*93Ow~_`WlvNM^h}j9uTB&V);A}N6f?h60bn) zfJGTo23~#GXNbobLf=AyOfA^|04kS;w!{7uc6%KlRD*rIe?30F79YR2vl2gS#}8Yb z%^7`RM>X2B{Cah0czx*L+R(w(ack&cW$27ObjC_F=jBK>lK5)uj{ci{U+!Cr?6xAi zS?`SiH#l0|Irgx3@Xos%qOhxHBN*t2ZE#3Lr8m5+Z1ix^?&TBJp2QdX?Vde$&%}Dq z(Y2nV)&3F48#1@!*kexI5qpYcgA=j$w9OZsH!kibb z5+i65jOTO|$ruo*#rvG67QYO@HHE&6PsP*9E-ZMRVz(BG-<(`ItVfHS_R^Bb057N3Ucv}0j#V-wq4mY0)Z@YPa9TexnxTP(&{is9AiD6o%PwU0{_PYv!Tmk^eEk)V2+o|_B+-yz*k z-LFvsED%OGBL#Fr1yTgYRqdy1sd2bsdPK2u-K{w0K2k;D*ylh diff --git a/src/util/__pycache__/ticktick.cpython-311.pyc b/src/util/__pycache__/ticktick.cpython-311.pyc deleted file mode 100644 index 97eb2ef21c13f4d172a2ec8a89d5e3566fea0b07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6241 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 None: - self._ticktick = ticktick - self._location_recorder = location_recorder - - async def process_message(self, message: Message) -> dict[str, str]: - if message.target == "ticktick": - return await self._process_ticktick_message(message=message) - if message.target == "location_recorder": - return await self._process_location(message=message) - return {"Status": "Unknown target"} - - async def trigger_webhook(self, payload: dict[str, str], webhook_id: str) -> None: - token: str = Config.get_env("HOMEASSISTANT_TOKEN") - webhook_url: str = Config.get_env("HOMEASSISTANT_URL") + "/api/webhook/" + webhook_id - headers: dict[str, str] = {"Authorization": f"Bearer {token}"} - await httpx.AsyncClient().post(webhook_url, json=payload, headers=headers) - - async def _process_ticktick_message(self, message: Message) -> dict[str, str]: - if message.action == "create_shopping_list": - return await self._create_shopping_list(content=message.content) - if message.action == "create_action_task": - return await self._create_action_task(content=message.content) - - return {"Status": "Unknown action"} - - async def _process_location(self, message: Message) -> dict[str, str]: - if message.action == "record": - location: dict[str, str] = ast.literal_eval(message.content) - await self._location_recorder.insert_location_now( - person=location["person"], - location=LocationData( - latitude=float(location["latitude"]), - longitude=float(location["longitude"]), - altitude=float(location["altitude"]), - ), - ) - return {"Status": "Location recorded"} - return {"Status": "Unknown action"} - - async def _create_shopping_list(self, content: str) -> dict[str, str]: - project_id = Config.get_env("TICKTICK_SHOPPING_LIST") - item: dict[str, str] = ast.literal_eval(content) - task = TickTick.Task(projectId=project_id, title=item["item"]) - return await self._ticktick.create_task(task=task) - - async def _create_action_task(self, content: str) -> dict[str, str]: - detail: dict[str, str] = ast.literal_eval(content) - project_id = Config.get_env("TICKTICK_HOME_TASK_LIST") - due_hour = detail["due_hour"] - due = datetime.now(tz=datetime.now().astimezone().tzinfo) + timedelta(hours=due_hour) - due = (due + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) - due = due.astimezone(timezone.utc) - task = TickTick.Task(projectId=project_id, title=detail["action"], dueDate=TickTick.datetime_to_ticktick_format(due)) - return await self._ticktick.create_task(task=task) diff --git a/src/util/location_recorder.py b/src/util/location_recorder.py deleted file mode 100644 index 940d34c..0000000 --- a/src/util/location_recorder.py +++ /dev/null @@ -1,120 +0,0 @@ -from dataclasses import dataclass -from datetime import UTC, datetime - -from sqlalchemy import REAL, TEXT, insert, text -from sqlalchemy.ext.asyncio import AsyncConnection, create_async_engine -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - - -class Base(DeclarativeBase): - pass - - -class Location(Base): - __tablename__ = "location" - person: Mapped[str] = mapped_column(type_=TEXT, primary_key=True, nullable=False) - datetime: Mapped[str] = mapped_column(type_=TEXT, primary_key=True, nullable=False) - latitude: Mapped[float] = mapped_column(type_=REAL, nullable=False) - longitude: Mapped[float] = mapped_column(type_=REAL, nullable=False) - altitude: Mapped[float] = mapped_column(type_=REAL, nullable=True) - - -@dataclass -class LocationData: - latitude: float - longitude: float - altitude: float - - -class LocationRecorder: - USER_VERSION = 3 - - def __init__(self, db_path: str) -> None: - self._db_path = "sqlite+aiosqlite:///" + db_path - - async def create_db_engine(self) -> None: - self._engine = create_async_engine(self._db_path) - async with self._engine.begin() as conn: - user_version = await self._get_user_version(conn=conn) - if user_version == 0: - await conn.run_sync(Base.metadata.create_all) - await self._set_user_version(conn=conn, user_version=2) - if user_version != LocationRecorder.USER_VERSION: - await self._migrate(conn=conn) - - async def dispose_db_engine(self) -> None: - await self._engine.dispose() - - async def insert_location(self, person: str, date_time: datetime, location: LocationData) -> None: - if date_time.tzinfo != UTC: - date_time = date_time.astimezone(UTC) - date_time_str = date_time.strftime("%Y-%m-%dT%H:%M:%S%z") - async with self._engine.begin() as conn: - await conn.execute( - insert(Location) - .prefix_with("OR IGNORE") - .values( - person=person, - datetime=date_time_str, - latitude=location.latitude, - longitude=location.longitude, - altitude=location.altitude, - ), - ) - - async def insert_locations(self, person: str, locations: dict[datetime, LocationData]) -> None: - async with self._engine.begin() as conn: - for k, v in locations.items(): - dt = k - if k.tzinfo != UTC: - dt = k.astimezone(UTC) - date_time_str = dt.strftime("%Y-%m-%dT%H:%M:%S%z") - await conn.execute( - insert(Location) - .prefix_with("OR IGNORE") - .values( - person=person, - datetime=date_time_str, - latitude=v.latitude, - longitude=v.longitude, - altitude=v.altitude, - ), - ) - - async def insert_location_now(self, person: str, location: LocationData) -> None: - now_utc = datetime.now(tz=UTC) - await self.insert_location(person, now_utc, location) - - async def _get_user_version(self, conn: AsyncConnection) -> int: - return (await conn.execute(text("PRAGMA user_version"))).first()[0] - - async def _set_user_version(self, conn: AsyncConnection, user_version: int) -> None: - await conn.execute(text("PRAGMA user_version = " + str(user_version))) - - async def _migrate(self, conn: AsyncConnection) -> None: - user_version = (await conn.execute(text("PRAGMA user_version"))).first()[0] - if user_version == 1: - await self._migrate_1_2(conn=conn) - user_version = (await conn.execute(text("PRAGMA user_version"))).first()[0] - if user_version == 2: # noqa: PLR2004 - await self._migrate_2_3(conn=conn) - user_version = (await conn.execute(text("PRAGMA user_version"))).first()[0] - - async def _migrate_1_2(self, conn: AsyncConnection) -> None: - print("Location Recorder: migrate from db ver 1 to 2.") - await conn.execute(text("DROP TABLE version")) - await conn.execute(text("ALTER TABLE location RENAME people TO person")) - await self._set_user_version(conn=conn, user_version=2) - - async def _migrate_2_3(self, conn: AsyncConnection) -> None: - print("Location Recorder: migrate from db ver 2 to 3.") - await conn.execute(text("ALTER TABLE location RENAME TO location_old")) - await conn.run_sync(Base.metadata.create_all) - await conn.execute( - text(""" - INSERT INTO location (person, datetime, latitude, longitude, altitude) - SELECT person, datetime, latitude, longitude, altitude FROM location_old; - """), - ) - await conn.execute(text("DROP TABLE location_old")) - await self._set_user_version(conn=conn, user_version=3) diff --git a/src/util/mqtt.py b/src/util/mqtt.py deleted file mode 100644 index f651608..0000000 --- a/src/util/mqtt.py +++ /dev/null @@ -1,73 +0,0 @@ -import queue -from dataclasses import dataclass - -from fastapi_mqtt import FastMQTT, MQTTConfig - - -@dataclass -class MQTTSubscription: - topic: str - callback: callable - subscribed: bool - - -@dataclass -class MQTTPendingMessage: - topic: str - payload: dict - retain: bool - - -class MQTT: - _instance = None - - def __new__(cls, *args, **kwargs): # noqa: ANN002, ANN003, ANN204 - if not cls._instance: - cls._instance = super().__new__(cls, *args, **kwargs) - return cls._instance - - def __init__(self) -> None: - self._mqtt_config = MQTTConfig(username="mqtt", password="mqtt", reconnect_retries=-1) # noqa: S106 - self._mqtt = FastMQTT(config=self._mqtt_config, client_id="home_automation_backend") - self._mqtt.mqtt_handlers.user_connect_handler = self.on_connect - self._mqtt.mqtt_handlers.user_message_handler = self.on_message - self._connected = False - self._subscribed_topic: dict[str, MQTTSubscription] = {} - self._queued_message: queue.Queue[MQTTPendingMessage] = queue.Queue() - - async def start(self) -> None: - print("MQTT Starting...") - await self._mqtt.mqtt_startup() - - async def stop(self) -> None: - print("MQTT Stopping...") - await self._mqtt.mqtt_shutdown() - - def on_connect(self, client, flags, rc, properties) -> None: # noqa: ANN001, ARG002 - print("Connected") - self._connected = True - while not self._queued_message.empty(): - msg = self._queued_message.get(block=False) - self.publish(msg.topic, msg.payload, retain=msg.retain) - for topic, subscription in self._subscribed_topic.items(): - if subscription.subscribed is False: - self.subscribe(topic, subscription.callback) - - async def on_message(self, client, topic: str, payload: bytes, qos: int, properties: any) -> any: # noqa: ANN001, ARG002 - print("On message") - if topic in self._subscribed_topic and self._subscribed_topic[topic].callback is not None: - await self._subscribed_topic[topic].callback(payload) - - def subscribe(self, topic: str, callback: callable) -> None: - if self._connected: - print("Subscribe to topic: ", topic) - self._mqtt.client.subscribe(topic) - self._subscribed_topic[topic] = MQTTSubscription(topic, callback, subscribed=True) - else: - self._subscribed_topic[topic] = MQTTSubscription(topic, callback, subscribed=False) - - def publish(self, topic: str, payload: dict, *, retain: bool) -> None: - if self._connected: - self._mqtt.publish(topic, payload=payload, retain=retain) - else: - self._queued_message.put(MQTTPendingMessage(topic, payload, retain=retain)) diff --git a/src/util/notion.py b/src/util/notion.py deleted file mode 100644 index 0e62e63..0000000 --- a/src/util/notion.py +++ /dev/null @@ -1,82 +0,0 @@ -from __future__ import annotations - -from dataclasses import asdict, dataclass, field - -from notion_client import AsyncClient as Client - - -@dataclass -class Text: - content: str - - -@dataclass -class RichText: - type: str - href: str | None = None - - -@dataclass -class RichTextText(RichText): - type: str = "text" - text: Text = field(default_factory=lambda: Text(content="")) - - -class NotionAsync: - def __init__(self, token: str) -> None: - self._client = Client(auth=token) - - def update_token(self, token: str) -> None: - self._client.aclose() - self._client = Client(auth=token) - - async def get_block(self, block_id: str) -> dict: - return await self._client.blocks.retrieve(block_id=block_id) - - async def get_block_children(self, block_id: str, start_cursor: str | None = None, page_size: int = 100) -> dict: - return await self._client.blocks.children.list( - block_id=block_id, - start_cursor=start_cursor, - page_size=page_size, - ) - - async def block_is_table(self, block_id: str) -> bool: - block: dict = await self.get_block(block_id=block_id) - return block["type"] == "table" - - async def get_table_width(self, table_id: str) -> int: - table = await self._client.blocks.retrieve(block_id=table_id) - return table["table"]["table_width"] - - async def append_table_row_text(self, table_id: str, text_list: list[str], after: str | None = None) -> None: - cells: list[RichText] = [] - for content in text_list: - cells.append([asdict(RichTextText(text=Text(content)))]) # noqa: PERF401 - await self.append_table_row(table_id=table_id, cells=cells, after=after) - - async def append_table_row(self, table_id: str, cells: list[RichText], after: str | None = None) -> None: - if not await self.block_is_table(table_id): - return - table_width = await self.get_table_width(table_id=table_id) - if table_width != len(cells): - return - children = [ - { - "object": "block", - "type": "table_row", - "table_row": { - "cells": cells, - }, - }, - ] - if after is None: - await self._client.blocks.children.append( - block_id=table_id, - children=children, - ) - else: - await self._client.blocks.children.append( - block_id=table_id, - children=children, - after=after, - ) diff --git a/src/util/notion/notion.go b/src/util/notion/notion.go new file mode 100644 index 0000000..a4b617f --- /dev/null +++ b/src/util/notion/notion.go @@ -0,0 +1,62 @@ +package notion + +import ( + "context" + + "github.com/jomei/notionapi" +) + +var client *notionapi.Client +var authToken string + +func Init(token string) { + authToken = token + client = notionapi.NewClient(notionapi.Token(token)) +} + +func GetClient() *notionapi.Client { + return client +} + +func AGetBlock(blockId string) chan struct { + Block notionapi.Block + Error error +} { + retval := make(chan struct { + Block notionapi.Block + Error error + }) + go func() { + block, err := client.Block.Get(context.Background(), notionapi.BlockID(blockId)) + retval <- struct { + Block notionapi.Block + Error error + }{block, err} + }() + return retval +} + +func AGetBlockChildren(blockId string, startCursor string, pageSize int) chan struct { + Children []notionapi.Block + NextCursor string + HasMore bool + Error error +} { + retval := make(chan struct { + Children []notionapi.Block + NextCursor string + HasMore bool + Error error + }) + pagination := notionapi.Pagination{StartCursor: notionapi.Cursor(startCursor), PageSize: pageSize} + go func() { + children, err := client.Block.GetChildren(context.Background(), notionapi.BlockID(blockId), &pagination) + retval <- struct { + Children []notionapi.Block + NextCursor string + HasMore bool + Error error + }{children.Results, children.NextCursor, children.HasMore, err} + }() + return retval +} diff --git a/src/util/notion/notion_test.go b/src/util/notion/notion_test.go new file mode 100644 index 0000000..0b57442 --- /dev/null +++ b/src/util/notion/notion_test.go @@ -0,0 +1,27 @@ +package notion_test + +import ( + "testing" + + "github.com/t-liu93/home-automation-backend/util/notion" +) + +func TestGetBlockBasic(t *testing.T) { + notion.Init("") + block := <-notion.AGetBlock("") + actObj := block.Block.GetObject() + expObj := "block" + if string(actObj) != expObj { + t.Errorf("Expected %s, but got %s", expObj, actObj) + } +} + +func TestGetBlockChildrenBasic(t *testing.T) { + notion.Init("") + blockChildren := <-notion.AGetBlockChildren("", "", 100) + actLen := len(blockChildren.Children) + expLen := 100 + if actLen != expLen { + t.Errorf("Expected %d, but got %d", expLen, actLen) + } +} diff --git a/src/util/tests/__init__.py b/src/util/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/util/tests/__pycache__/__init__.cpython-311.pyc b/src/util/tests/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 42ba0cb6a4a83fdde3a5b24db11942e83c018ab7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 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 YTh13+{vGw<9ofM7LN*1VCEd>d2Zi00-~a#s diff --git a/src/util/tests/test_location_recorder.py b/src/util/tests/test_location_recorder.py deleted file mode 100644 index 71d78b5..0000000 --- a/src/util/tests/test_location_recorder.py +++ /dev/null @@ -1,354 +0,0 @@ -import asyncio -import sqlite3 -from datetime import UTC, datetime -from pathlib import Path -from zoneinfo import ZoneInfo - -import pytest -from sqlalchemy import INTEGER, REAL, TEXT, create_engine, text -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - -from src.util.location_recorder import LocationData, LocationRecorder - -DB_PATH = Path(__file__).resolve().parent / "test.db" -DB_PATH_STR = str(DB_PATH) - - -@pytest.fixture -def _reset_event_loop() -> any: - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - loop.stop() - loop.close() - except RuntimeError: - pass - asyncio.set_event_loop(asyncio.new_event_loop()) - - -@pytest.fixture -def _teardown() -> any: - yield - DB_PATH.unlink() - - -@pytest.fixture -def _create_v1_db() -> None: - db = "sqlite:///" + DB_PATH_STR - - class Base(DeclarativeBase): - pass - - class Version(Base): - __tablename__ = "version" - version_type: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) - version: Mapped[int] = mapped_column(type_=INTEGER) - - class Location(Base): - __tablename__ = "location" - people: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) - datetime: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) - latitude: Mapped[float] = mapped_column(type_=REAL) - longitude: Mapped[float] = mapped_column(type_=REAL) - altitude: Mapped[float] = mapped_column(type_=REAL) - - engine = create_engine(db) - - Base.metadata.create_all(engine) - with engine.begin() as conn: - conn.execute(text("PRAGMA user_version = 1")) - - -@pytest.fixture -def _create_v2_db() -> None: - db = "sqlite:///" + DB_PATH_STR - - class Base(DeclarativeBase): - pass - - class Location(Base): - __tablename__ = "location" - person: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) - datetime: Mapped[str] = mapped_column(type_=TEXT, primary_key=True) - latitude: Mapped[float] = mapped_column(type_=REAL) - longitude: Mapped[float] = mapped_column(type_=REAL) - altitude: Mapped[float] = mapped_column(type_=REAL) - - engine = create_engine(db) - - Base.metadata.create_all(engine) - with engine.begin() as conn: - conn.execute(text("PRAGMA user_version = 2")) - - -@pytest.fixture -def _create_latest_db() -> None: - db = "sqlite:///" + DB_PATH_STR - - class Base(DeclarativeBase): - pass - - class Location(Base): - __tablename__ = "location" - person: Mapped[str] = mapped_column(type_=TEXT, primary_key=True, nullable=False) - datetime: Mapped[str] = mapped_column(type_=TEXT, primary_key=True, nullable=False) - latitude: Mapped[float] = mapped_column(type_=REAL, nullable=False) - longitude: Mapped[float] = mapped_column(type_=REAL, nullable=False) - altitude: Mapped[float] = mapped_column(type_=REAL, nullable=True) - - engine = create_engine(db) - - Base.metadata.create_all(engine) - - -@pytest.mark.usefixtures("_create_v1_db") -@pytest.mark.usefixtures("_teardown") -def test_migration_1_latest() -> None: - nr_tables_ver_1 = 2 - table_ver_1_0 = "version" - nr_column_ver_1_version = 2 - table_ver_1_1 = "location" - nr_column_ver_1_location = 5 - nr_tables_ver_2 = 1 - table_ver_2_0 = "location" - - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("PRAGMA user_version") - assert sqlite3_cursor.fetchone()[0] == 1 - sqlite3_cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table';") - tables = sqlite3_cursor.fetchall() - assert len(tables) == nr_tables_ver_1 - assert tables[0][0] == table_ver_1_0 - assert tables[1][0] == table_ver_1_1 - sqlite3_cursor.execute(f"PRAGMA table_info({table_ver_1_0})") - table_info_version = sqlite3_cursor.fetchall() - assert len(table_info_version) == nr_column_ver_1_version - assert table_info_version[0] == (0, "version_type", "TEXT", 1, None, 1) - assert table_info_version[1] == (1, "version", "INTEGER", 1, None, 0) - sqlite3_cursor.execute(f"PRAGMA table_info({table_ver_1_1})") - table_info_location = sqlite3_cursor.fetchall() - assert len(table_info_location) == nr_column_ver_1_location - assert table_info_location[0] == (0, "people", "TEXT", 1, None, 1) - assert table_info_location[1] == (1, "datetime", "TEXT", 1, None, 2) - assert table_info_location[2] == (2, "latitude", "REAL", 1, None, 0) - assert table_info_location[3] == (3, "longitude", "REAL", 1, None, 0) - assert table_info_location[4] == (4, "altitude", "REAL", 1, None, 0) - - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - asyncio.run(location_recorder.create_db_engine()) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("PRAGMA user_version") - assert sqlite3_cursor.fetchone()[0] == LocationRecorder.USER_VERSION - sqlite3_cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table';") - tables = sqlite3_cursor.fetchall() - assert len(tables) == nr_tables_ver_2 - sqlite3_cursor.execute(f"PRAGMA table_info({table_ver_2_0})") - table_info_location = sqlite3_cursor.fetchall() - assert len(table_info_location) == nr_column_ver_1_location - assert table_info_location[0] == (0, "person", "TEXT", 1, None, 1) - assert table_info_location[1] == (1, "datetime", "TEXT", 1, None, 2) - assert table_info_location[2] == (2, "latitude", "REAL", 1, None, 0) - assert table_info_location[3] == (3, "longitude", "REAL", 1, None, 0) - assert table_info_location[4] == (4, "altitude", "REAL", 0, None, 0) - sqlite3_cursor.close() - - -@pytest.mark.usefixtures("_create_v2_db") -@pytest.mark.usefixtures("_teardown") -def test_migration_2_latest() -> None: - nr_tables_ver_2 = 1 - table_ver_2_0 = "location" - nr_column_ver_2_location = 5 - nr_tables_ver_3 = 1 - table_ver_3_0 = "location" - nr_column_ver_3_location = 5 - - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("PRAGMA user_version") - assert sqlite3_cursor.fetchone()[0] == 2 # noqa: PLR2004 - sqlite3_cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table';") - tables = sqlite3_cursor.fetchall() - assert len(tables) == nr_tables_ver_2 - assert tables[0][0] == table_ver_2_0 - sqlite3_cursor.execute(f"PRAGMA table_info({table_ver_2_0})") - table_info_location = sqlite3_cursor.fetchall() - assert len(table_info_location) == nr_column_ver_2_location - assert table_info_location[0] == (0, "person", "TEXT", 1, None, 1) - assert table_info_location[1] == (1, "datetime", "TEXT", 1, None, 2) - assert table_info_location[2] == (2, "latitude", "REAL", 1, None, 0) - assert table_info_location[3] == (3, "longitude", "REAL", 1, None, 0) - assert table_info_location[4] == (4, "altitude", "REAL", 1, None, 0) - - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - asyncio.run(location_recorder.create_db_engine()) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("PRAGMA user_version") - assert sqlite3_cursor.fetchone()[0] == LocationRecorder.USER_VERSION - sqlite3_cursor.execute("SELECT name FROM sqlite_master WHERE type = 'table';") - tables = sqlite3_cursor.fetchall() - assert len(tables) == nr_tables_ver_3 - sqlite3_cursor.execute(f"PRAGMA table_info({table_ver_3_0})") - table_info_location = sqlite3_cursor.fetchall() - assert len(table_info_location) == nr_column_ver_3_location - assert table_info_location[0] == (0, "person", "TEXT", 1, None, 1) - assert table_info_location[1] == (1, "datetime", "TEXT", 1, None, 2) - assert table_info_location[2] == (2, "latitude", "REAL", 1, None, 0) - assert table_info_location[3] == (3, "longitude", "REAL", 1, None, 0) - assert table_info_location[4] == (4, "altitude", "REAL", 0, None, 0) - sqlite3_cursor.close() - - -@pytest.mark.usefixtures("_reset_event_loop") -@pytest.mark.usefixtures("_teardown") -def test_create_db() -> None: - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - event_loop = asyncio.get_event_loop() - event_loop.run_until_complete(location_recorder.create_db_engine()) - event_loop.run_until_complete(location_recorder.dispose_db_engine()) - assert DB_PATH.exists() - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("PRAGMA user_version") - assert sqlite3_cursor.fetchone()[0] == LocationRecorder.USER_VERSION - - -@pytest.mark.usefixtures("_reset_event_loop") -@pytest.mark.usefixtures("_create_latest_db") -@pytest.mark.usefixtures("_teardown") -def test_inser_location_utc() -> None: - latitude = 1.0 - longitude = 2.0 - altitude = 3.0 - person = "test_person" - date_time = datetime.now(tz=UTC) - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - event_loop = asyncio.get_event_loop() - event_loop.run_until_complete(location_recorder.create_db_engine()) - location_data = LocationData(latitude=latitude, longitude=longitude, altitude=altitude) - event_loop.run_until_complete( - location_recorder.insert_location( - person=person, - date_time=date_time, - location=location_data, - ), - ) - event_loop.run_until_complete(location_recorder.dispose_db_engine()) - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("SELECT * FROM location") - location = sqlite3_cursor.fetchone() - assert location[0] == person - assert location[1] == date_time.strftime("%Y-%m-%dT%H:%M:%S%z") - assert location[2] == latitude - assert location[3] == longitude - assert location[4] == altitude - sqlite3_cursor.close() - - -@pytest.mark.usefixtures("_reset_event_loop") -@pytest.mark.usefixtures("_create_latest_db") -@pytest.mark.usefixtures("_teardown") -def test_inser_location_other() -> None: - latitude = 1.0 - longitude = 2.0 - altitude = 3.0 - person = "test_person" - tz = ZoneInfo("Asia/Shanghai") - date_time = datetime.now(tz=tz) - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - event_loop = asyncio.get_event_loop() - event_loop.run_until_complete(location_recorder.create_db_engine()) - location_data = LocationData(latitude=latitude, longitude=longitude, altitude=altitude) - event_loop.run_until_complete( - location_recorder.insert_location( - person=person, - date_time=date_time, - location=location_data, - ), - ) - event_loop.run_until_complete(location_recorder.dispose_db_engine()) - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("SELECT * FROM location") - location = sqlite3_cursor.fetchone() - assert location[0] == person - assert location[1] == date_time.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S%z") - assert location[2] == latitude - assert location[3] == longitude - assert location[4] == altitude - sqlite3_cursor.close() - - -@pytest.mark.usefixtures("_reset_event_loop") -@pytest.mark.usefixtures("_create_latest_db") -@pytest.mark.usefixtures("_teardown") -def test_insert_location_now() -> None: - latitude = 1.0 - longitude = 2.0 - altitude = 3.0 - person = "test_person" - date_time = datetime.now(tz=UTC) - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - event_loop = asyncio.get_event_loop() - event_loop.run_until_complete(location_recorder.create_db_engine()) - location_data = LocationData(latitude=latitude, longitude=longitude, altitude=altitude) - event_loop.run_until_complete( - location_recorder.insert_location_now( - person=person, - location=location_data, - ), - ) - event_loop.run_until_complete(location_recorder.dispose_db_engine()) - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("SELECT * FROM location") - location = sqlite3_cursor.fetchone() - assert location[0] == person - date_time_act = datetime.strptime(location[1], "%Y-%m-%dT%H:%M:%S%z") - assert date_time.date() == date_time_act.date() - assert location[2] == latitude - assert location[3] == longitude - assert location[4] == altitude - sqlite3_cursor.close() - - -@pytest.mark.usefixtures("_reset_event_loop") -@pytest.mark.usefixtures("_create_latest_db") -@pytest.mark.usefixtures("_teardown") -def test_insert_locations() -> None: - locations: dict[datetime, LocationData] = {} - person = "Tianyu" - time_0 = datetime.now(tz=UTC) - lat_0 = 1.0 - lon_0 = 2.0 - alt_0 = 3.0 - time_1 = datetime(2021, 8, 30, 10, 20, 15, tzinfo=UTC) - lat_1 = 155.0 - lon_1 = 33.36 - alt_1 = 1058 - locations[time_0] = LocationData(lat_0, lon_0, alt_0) - locations[time_1] = LocationData(lat_1, lon_1, alt_1) - location_recorder = LocationRecorder(db_path=DB_PATH_STR) - event_loop = asyncio.get_event_loop() - event_loop.run_until_complete(location_recorder.create_db_engine()) - event_loop.run_until_complete( - location_recorder.insert_locations(person=person, locations=locations), - ) - sqlite3_db = sqlite3.connect(DB_PATH_STR) - sqlite3_cursor = sqlite3_db.cursor() - sqlite3_cursor.execute("SELECT * FROM location") - locations = sqlite3_cursor.fetchall() - assert len(locations) == 2 # noqa: PLR2004 - assert locations[0][0] == person - assert locations[0][1] == time_0.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S%z") - assert locations[0][2] == lat_0 - assert locations[0][3] == lon_0 - assert locations[0][4] == alt_0 - assert locations[1][0] == person - assert locations[1][1] == time_1.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S%z") - assert locations[1][2] == lat_1 - assert locations[1][3] == lon_1 - assert locations[1][4] == alt_1 - sqlite3_cursor.close() diff --git a/src/util/ticktick.py b/src/util/ticktick.py deleted file mode 100644 index 7ef6245..0000000 --- a/src/util/ticktick.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import annotations - -import urllib.parse -from dataclasses import asdict, dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from datetime import datetime - -import httpx - -from src.config import Config - - -class TickTick: - @dataclass - class Task: - projectId: str # noqa: N815 - title: str - dueDate: str | None = None # noqa: N815 - content: str | None = None - desc: str | None = None - - def __init__(self) -> None: - print("Initializing TickTick...") - if Config.get_env("TICKTICK_ACCESS_TOKEN") is None: - self._begin_auth() - else: - self._access_token = Config.get_env("TICKTICK_ACCESS_TOKEN") - - def _begin_auth(self) -> None: - ticktick_code_auth_url = "https://ticktick.com/oauth/authorize?" - ticktick_code_auth_params = { - "client_id": Config.get_env("TICKTICK_CLIENT_ID"), - "scope": "tasks:read tasks:write", - "state": "begin_auth", - "redirect_uri": Config.get_env("TICKTICK_CODE_REDIRECT_URI"), - "response_type": "code", - } - ticktick_auth_url_encoded = urllib.parse.urlencode(ticktick_code_auth_params) - print("Visit: ", ticktick_code_auth_url + ticktick_auth_url_encoded, " to authenticate.") - - async def retrieve_access_token(self, code: str, state: str) -> bool: - if state != "begin_auth": - print("Invalid state.") - return False - ticktick_token_url = "https://ticktick.com/oauth/token" # noqa: S105 - ticktick_token_auth_params: dict[str, str] = { - "code": code, - "grant_type": "authorization_code", - "scope": "tasks:write tasks:read", - "redirect_uri": Config.get_env("TICKTICK_CODE_REDIRECT_URI"), - } - client_id = Config.get_env("TICKTICK_CLIENT_ID") - client_secret = Config.get_env("TICKTICK_CLIENT_SECRET") - response = await httpx.AsyncClient().post( - ticktick_token_url, - data=ticktick_token_auth_params, - auth=httpx.BasicAuth(username=client_id, password=client_secret), - timeout=10, - ) - Config.update_env("TICKTICK_ACCESS_TOKEN", response.json()["access_token"]) - return True - - async def get_tasks(self, project_id: str) -> list[dict]: - ticktick_get_tasks_url = "https://api.ticktick.com/open/v1/project/" + project_id + "/data" - header: dict[str, str] = {"Authorization": f"Bearer {self._access_token}"} - response = await httpx.AsyncClient().get(ticktick_get_tasks_url, headers=header, timeout=10) - return response.json()["tasks"] - - async def has_duplicate_task(self, project_id: str, task_title: str) -> bool: - tasks = await self.get_tasks(project_id=project_id) - return any(task["title"] == task_title for task in tasks) - - async def create_task(self, task: TickTick.Task) -> dict[str, str]: - if not await self.has_duplicate_task(project_id=task.projectId, task_title=task.title): - ticktick_task_creation_url = "https://api.ticktick.com/open/v1/task" - header: dict[str, str] = {"Authorization": f"Bearer {self._access_token}"} - await httpx.AsyncClient().post(ticktick_task_creation_url, headers=header, json=asdict(task), timeout=10) - return {"title": task.title} - - @staticmethod - def datetime_to_ticktick_format(datetime: datetime) -> str: - return datetime.strftime("%Y-%m-%dT%H:%M:%S") + "+0000"