Compare commits
1 Commits
refactorin
...
95d0fa07b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 95d0fa07b8 |
20
.github/workflows/short-tests.yml
vendored
Normal file
20
.github/workflows/short-tests.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Run short tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
working-directory: ./src
|
||||||
|
run: go test -v --short ./...
|
||||||
46
.gitignore
vendored
46
.gitignore
vendored
@@ -1,21 +1,37 @@
|
|||||||
# Environments
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
temp_data/
|
||||||
|
|
||||||
|
# py file for branch switching
|
||||||
.venv
|
.venv
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
config.yaml
|
||||||
|
bin/
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
|
||||||
|
|
||||||
*.egg-info/
|
cover.html
|
||||||
|
|
||||||
devsettings.yaml
|
|
||||||
13
.vscode/extensions.json
vendored
13
.vscode/extensions.json
vendored
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
|
||||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
|
||||||
// List of extensions which should be recommended for users of this workspace.
|
|
||||||
"recommendations": [
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.debugpy",
|
|
||||||
"charliermarsh.ruff",
|
|
||||||
],
|
|
||||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
||||||
37
.vscode/launch.json
vendored
37
.vscode/launch.json
vendored
@@ -5,22 +5,31 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Python Debugger: FastAPI",
|
"name": "Launch Package",
|
||||||
"type": "debugpy",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Launch Poo Reverse",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/src/helper/poo_recorder_helper/main.go",
|
||||||
"args": [
|
"args": [
|
||||||
"app:app",
|
"reverse"
|
||||||
"--host=0.0.0.0",
|
]
|
||||||
"--reload",
|
},
|
||||||
"--port=18881"
|
{
|
||||||
],
|
"name": "Launch Home Automation",
|
||||||
"jinja": true,
|
"type": "go",
|
||||||
"autoStartBrowser": false,
|
"request": "launch",
|
||||||
"env": {
|
"mode": "auto",
|
||||||
"CONFIG_FILE": "devsettings.yaml"
|
"program": "${workspaceFolder}/src/main.go",
|
||||||
},
|
"args": [
|
||||||
"console": "integratedTerminal"
|
"serve"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
16
.vscode/settings.json
vendored
16
.vscode/settings.json
vendored
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"[python]": {
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll": "explicit",
|
|
||||||
"source.organizeImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
|
||||||
},
|
|
||||||
"python.testing.pytestArgs": [
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"python.testing.unittestEnabled": false,
|
|
||||||
"python.testing.pytestEnabled": true,
|
|
||||||
"python.analysis.typeCheckingMode": "standard",
|
|
||||||
}
|
|
||||||
@@ -1,3 +1 @@
|
|||||||
# Home Automation Backend
|
Port 8881
|
||||||
|
|
||||||

|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""`app` package for the project."""
|
|
||||||
|
|
||||||
__all__ = ["main"]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""API package for routers."""
|
|
||||||
|
|
||||||
__all__ = ["health"]
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health", tags=["health"])
|
|
||||||
async def health_check():
|
|
||||||
"""Minimal health endpoint."""
|
|
||||||
return {"status": "ok"}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""Core package for settings and helpers."""
|
|
||||||
|
|
||||||
__all__ = ["config"]
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
PROJECT_NAME: str = "home-automation-backend"
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
|
||||||
18
app/main.py
18
app/main.py
@@ -1,18 +0,0 @@
|
|||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
from app.api.health import router as health_router
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
|
||||||
app = FastAPI(title=settings.PROJECT_NAME)
|
|
||||||
app.include_router(health_router, prefix="/api")
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"message": "Welcome to the minimal FastAPI template"}
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-r requirements.in
|
|
||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.11
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --generate-hashes --output-file=dev-requirements.txt dev-requirements.in
|
|
||||||
#
|
|
||||||
annotated-doc==0.0.4 \
|
|
||||||
--hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \
|
|
||||||
--hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4
|
|
||||||
# via fastapi
|
|
||||||
annotated-types==0.7.0 \
|
|
||||||
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
|
|
||||||
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
|
|
||||||
# via pydantic
|
|
||||||
anyio==4.12.1 \
|
|
||||||
--hash=sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703 \
|
|
||||||
--hash=sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c
|
|
||||||
# via
|
|
||||||
# httpx
|
|
||||||
# starlette
|
|
||||||
argon2-cffi==25.1.0 \
|
|
||||||
--hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \
|
|
||||||
--hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741
|
|
||||||
# via -r requirements.in
|
|
||||||
argon2-cffi-bindings==25.1.0 \
|
|
||||||
--hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \
|
|
||||||
--hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \
|
|
||||||
--hash=sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d \
|
|
||||||
--hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \
|
|
||||||
--hash=sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a \
|
|
||||||
--hash=sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f \
|
|
||||||
--hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \
|
|
||||||
--hash=sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690 \
|
|
||||||
--hash=sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584 \
|
|
||||||
--hash=sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e \
|
|
||||||
--hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \
|
|
||||||
--hash=sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f \
|
|
||||||
--hash=sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623 \
|
|
||||||
--hash=sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b \
|
|
||||||
--hash=sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44 \
|
|
||||||
--hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \
|
|
||||||
--hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \
|
|
||||||
--hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \
|
|
||||||
--hash=sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6 \
|
|
||||||
--hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \
|
|
||||||
--hash=sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85 \
|
|
||||||
--hash=sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92 \
|
|
||||||
--hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \
|
|
||||||
--hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a \
|
|
||||||
--hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \
|
|
||||||
--hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb
|
|
||||||
# via argon2-cffi
|
|
||||||
certifi==2026.1.4 \
|
|
||||||
--hash=sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c \
|
|
||||||
--hash=sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120
|
|
||||||
# via
|
|
||||||
# httpcore
|
|
||||||
# httpx
|
|
||||||
cffi==2.0.0 \
|
|
||||||
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
|
|
||||||
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
|
|
||||||
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
|
|
||||||
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
|
|
||||||
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
|
|
||||||
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
|
|
||||||
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
|
|
||||||
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
|
|
||||||
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
|
|
||||||
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
|
||||||
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
|
|
||||||
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
|
|
||||||
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
|
|
||||||
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
|
|
||||||
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
|
||||||
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
|
|
||||||
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
|
|
||||||
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
|
|
||||||
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
|
||||||
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
|
|
||||||
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
|
|
||||||
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
|
||||||
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
|
||||||
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
|
|
||||||
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
|
|
||||||
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
|
||||||
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
|
|
||||||
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
|
|
||||||
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
|
||||||
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
|
|
||||||
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
|
|
||||||
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
|
|
||||||
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
|
|
||||||
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
|
|
||||||
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
|
|
||||||
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
|
|
||||||
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
|
||||||
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
|
|
||||||
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
|
|
||||||
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
|
|
||||||
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
|
|
||||||
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
|
|
||||||
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
|
|
||||||
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
|
|
||||||
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
|
||||||
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
|
|
||||||
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
|
|
||||||
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
|
|
||||||
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
|
||||||
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
|
|
||||||
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
|
|
||||||
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
|
|
||||||
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
|
|
||||||
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
|
|
||||||
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
|
|
||||||
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
|
|
||||||
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
|
|
||||||
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
|
|
||||||
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
|
||||||
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
|
|
||||||
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
|
|
||||||
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
|
|
||||||
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
|
|
||||||
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
|
|
||||||
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
|
|
||||||
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
|
|
||||||
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
|
|
||||||
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
|
|
||||||
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
|
|
||||||
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
|
|
||||||
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
|
|
||||||
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
|
|
||||||
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
|
||||||
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
|
|
||||||
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
|
|
||||||
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
|
|
||||||
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
|
|
||||||
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
|
|
||||||
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
|
|
||||||
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
|
|
||||||
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
|
|
||||||
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
|
|
||||||
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
|
||||||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
|
||||||
# via argon2-cffi-bindings
|
|
||||||
click==8.3.1 \
|
|
||||||
--hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \
|
|
||||||
--hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6
|
|
||||||
# via uvicorn
|
|
||||||
fastapi==0.128.0 \
|
|
||||||
--hash=sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a \
|
|
||||||
--hash=sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d
|
|
||||||
# via -r requirements.in
|
|
||||||
greenlet==3.3.0 \
|
|
||||||
--hash=sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b \
|
|
||||||
--hash=sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527 \
|
|
||||||
--hash=sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365 \
|
|
||||||
--hash=sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221 \
|
|
||||||
--hash=sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd \
|
|
||||||
--hash=sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53 \
|
|
||||||
--hash=sha256:2de5a0b09eab81fc6a382791b995b1ccf2b172a9fec934747a7a23d2ff291794 \
|
|
||||||
--hash=sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492 \
|
|
||||||
--hash=sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3 \
|
|
||||||
--hash=sha256:39b28e339fc3c348427560494e28d8a6f3561c8d2bcf7d706e1c624ed8d822b9 \
|
|
||||||
--hash=sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3 \
|
|
||||||
--hash=sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b \
|
|
||||||
--hash=sha256:4243050a88ba61842186cb9e63c7dfa677ec146160b0efd73b855a3d9c7fcf32 \
|
|
||||||
--hash=sha256:4449a736606bd30f27f8e1ff4678ee193bc47f6ca810d705981cfffd6ce0d8c5 \
|
|
||||||
--hash=sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8 \
|
|
||||||
--hash=sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955 \
|
|
||||||
--hash=sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f \
|
|
||||||
--hash=sha256:670d0f94cd302d81796e37299bcd04b95d62403883b24225c6b5271466612f45 \
|
|
||||||
--hash=sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9 \
|
|
||||||
--hash=sha256:6cb3a8ec3db4a3b0eb8a3c25436c2d49e3505821802074969db017b87bc6a948 \
|
|
||||||
--hash=sha256:6f8496d434d5cb2dce025773ba5597f71f5410ae499d5dd9533e0653258cdb3d \
|
|
||||||
--hash=sha256:73631cd5cccbcfe63e3f9492aaa664d278fda0ce5c3d43aeda8e77317e38efbd \
|
|
||||||
--hash=sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170 \
|
|
||||||
--hash=sha256:7652ee180d16d447a683c04e4c5f6441bae7ba7b17ffd9f6b3aff4605e9e6f71 \
|
|
||||||
--hash=sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54 \
|
|
||||||
--hash=sha256:7dee147740789a4632cace364816046e43310b59ff8fb79833ab043aefa72fd5 \
|
|
||||||
--hash=sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614 \
|
|
||||||
--hash=sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3 \
|
|
||||||
--hash=sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38 \
|
|
||||||
--hash=sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808 \
|
|
||||||
--hash=sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739 \
|
|
||||||
--hash=sha256:a687205fb22794e838f947e2194c0566d3812966b41c78709554aa883183fb62 \
|
|
||||||
--hash=sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39 \
|
|
||||||
--hash=sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb \
|
|
||||||
--hash=sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39 \
|
|
||||||
--hash=sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55 \
|
|
||||||
--hash=sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb \
|
|
||||||
--hash=sha256:b299a0cb979f5d7197442dccc3aee67fce53500cd88951b7e6c35575701c980b \
|
|
||||||
--hash=sha256:b3c374782c2935cc63b2a27ba8708471de4ad1abaa862ffdb1ef45a643ddbb7d \
|
|
||||||
--hash=sha256:b49e7ed51876b459bd645d83db257f0180e345d3f768a35a85437a24d5a49082 \
|
|
||||||
--hash=sha256:b96dc7eef78fd404e022e165ec55327f935b9b52ff355b067eb4a0267fc1cffb \
|
|
||||||
--hash=sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7 \
|
|
||||||
--hash=sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc \
|
|
||||||
--hash=sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931 \
|
|
||||||
--hash=sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388 \
|
|
||||||
--hash=sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45 \
|
|
||||||
--hash=sha256:e29f3018580e8412d6aaf5641bb7745d38c85228dacf51a73bd4e26ddf2a6a8e \
|
|
||||||
--hash=sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655
|
|
||||||
# via sqlalchemy
|
|
||||||
h11==0.16.0 \
|
|
||||||
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
|
||||||
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
|
||||||
# via
|
|
||||||
# httpcore
|
|
||||||
# uvicorn
|
|
||||||
httpcore==1.0.9 \
|
|
||||||
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
|
|
||||||
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
|
|
||||||
# via httpx
|
|
||||||
httpx==0.28.1 \
|
|
||||||
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
|
|
||||||
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
|
|
||||||
# via -r requirements.in
|
|
||||||
idna==3.11 \
|
|
||||||
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
|
|
||||||
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
|
|
||||||
# via
|
|
||||||
# anyio
|
|
||||||
# httpx
|
|
||||||
iniconfig==2.3.0 \
|
|
||||||
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
|
||||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
|
||||||
# via pytest
|
|
||||||
packaging==25.0 \
|
|
||||||
--hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
|
|
||||||
--hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
|
|
||||||
# via pytest
|
|
||||||
pluggy==1.6.0 \
|
|
||||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
|
||||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
|
||||||
# via pytest
|
|
||||||
pycparser==2.23 \
|
|
||||||
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
|
||||||
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
|
||||||
# via cffi
|
|
||||||
pydantic==2.12.5 \
|
|
||||||
--hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \
|
|
||||||
--hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d
|
|
||||||
# via
|
|
||||||
# fastapi
|
|
||||||
# pydantic-settings
|
|
||||||
# sqlmodel
|
|
||||||
pydantic-core==2.41.5 \
|
|
||||||
--hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \
|
|
||||||
--hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \
|
|
||||||
--hash=sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504 \
|
|
||||||
--hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \
|
|
||||||
--hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \
|
|
||||||
--hash=sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c \
|
|
||||||
--hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \
|
|
||||||
--hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \
|
|
||||||
--hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \
|
|
||||||
--hash=sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a \
|
|
||||||
--hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \
|
|
||||||
--hash=sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2 \
|
|
||||||
--hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \
|
|
||||||
--hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \
|
|
||||||
--hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \
|
|
||||||
--hash=sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba \
|
|
||||||
--hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \
|
|
||||||
--hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \
|
|
||||||
--hash=sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963 \
|
|
||||||
--hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \
|
|
||||||
--hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \
|
|
||||||
--hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \
|
|
||||||
--hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \
|
|
||||||
--hash=sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2 \
|
|
||||||
--hash=sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5 \
|
|
||||||
--hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \
|
|
||||||
--hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \
|
|
||||||
--hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \
|
|
||||||
--hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \
|
|
||||||
--hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \
|
|
||||||
--hash=sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093 \
|
|
||||||
--hash=sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5 \
|
|
||||||
--hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \
|
|
||||||
--hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \
|
|
||||||
--hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \
|
|
||||||
--hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \
|
|
||||||
--hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \
|
|
||||||
--hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \
|
|
||||||
--hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \
|
|
||||||
--hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \
|
|
||||||
--hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \
|
|
||||||
--hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \
|
|
||||||
--hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \
|
|
||||||
--hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \
|
|
||||||
--hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \
|
|
||||||
--hash=sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97 \
|
|
||||||
--hash=sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a \
|
|
||||||
--hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \
|
|
||||||
--hash=sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9 \
|
|
||||||
--hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \
|
|
||||||
--hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \
|
|
||||||
--hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \
|
|
||||||
--hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \
|
|
||||||
--hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \
|
|
||||||
--hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \
|
|
||||||
--hash=sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941 \
|
|
||||||
--hash=sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3 \
|
|
||||||
--hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \
|
|
||||||
--hash=sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3 \
|
|
||||||
--hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \
|
|
||||||
--hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \
|
|
||||||
--hash=sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146 \
|
|
||||||
--hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \
|
|
||||||
--hash=sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60 \
|
|
||||||
--hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \
|
|
||||||
--hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \
|
|
||||||
--hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \
|
|
||||||
--hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \
|
|
||||||
--hash=sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460 \
|
|
||||||
--hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \
|
|
||||||
--hash=sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf \
|
|
||||||
--hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \
|
|
||||||
--hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \
|
|
||||||
--hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \
|
|
||||||
--hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \
|
|
||||||
--hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \
|
|
||||||
--hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \
|
|
||||||
--hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \
|
|
||||||
--hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \
|
|
||||||
--hash=sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d \
|
|
||||||
--hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \
|
|
||||||
--hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \
|
|
||||||
--hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \
|
|
||||||
--hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \
|
|
||||||
--hash=sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8 \
|
|
||||||
--hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \
|
|
||||||
--hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \
|
|
||||||
--hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \
|
|
||||||
--hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \
|
|
||||||
--hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \
|
|
||||||
--hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \
|
|
||||||
--hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \
|
|
||||||
--hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \
|
|
||||||
--hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \
|
|
||||||
--hash=sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a \
|
|
||||||
--hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \
|
|
||||||
--hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \
|
|
||||||
--hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \
|
|
||||||
--hash=sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a \
|
|
||||||
--hash=sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556 \
|
|
||||||
--hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \
|
|
||||||
--hash=sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49 \
|
|
||||||
--hash=sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2 \
|
|
||||||
--hash=sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9 \
|
|
||||||
--hash=sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b \
|
|
||||||
--hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \
|
|
||||||
--hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \
|
|
||||||
--hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \
|
|
||||||
--hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \
|
|
||||||
--hash=sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82 \
|
|
||||||
--hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \
|
|
||||||
--hash=sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b \
|
|
||||||
--hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \
|
|
||||||
--hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \
|
|
||||||
--hash=sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5 \
|
|
||||||
--hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \
|
|
||||||
--hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \
|
|
||||||
--hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \
|
|
||||||
--hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 \
|
|
||||||
--hash=sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425 \
|
|
||||||
--hash=sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52
|
|
||||||
# via pydantic
|
|
||||||
pydantic-settings==2.12.0 \
|
|
||||||
--hash=sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0 \
|
|
||||||
--hash=sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809
|
|
||||||
# via -r requirements.in
|
|
||||||
pygments==2.19.2 \
|
|
||||||
--hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \
|
|
||||||
--hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b
|
|
||||||
# via pytest
|
|
||||||
pytest==9.0.2 \
|
|
||||||
--hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \
|
|
||||||
--hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11
|
|
||||||
# via
|
|
||||||
# -r dev-requirements.in
|
|
||||||
# pytest-asyncio
|
|
||||||
pytest-asyncio==1.3.0 \
|
|
||||||
--hash=sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5 \
|
|
||||||
--hash=sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5
|
|
||||||
# via -r dev-requirements.in
|
|
||||||
python-dotenv==1.2.1 \
|
|
||||||
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
|
|
||||||
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
|
|
||||||
# via pydantic-settings
|
|
||||||
pyyaml==6.0.3 \
|
|
||||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
|
||||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
|
||||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
|
||||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
|
||||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
|
||||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
|
||||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
|
||||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
|
||||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
|
||||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
|
||||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
|
||||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
|
||||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
|
||||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
|
||||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
|
||||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
|
||||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
|
||||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
|
||||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
|
||||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
|
||||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
|
||||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
|
||||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
|
||||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
|
||||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
|
||||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
|
||||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
|
||||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
|
||||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
|
||||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
|
||||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
|
||||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
|
||||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
|
||||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
|
||||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
|
||||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
|
||||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
|
||||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
|
||||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
|
||||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
|
||||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
|
||||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
|
||||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
|
||||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
|
||||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
|
||||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
|
||||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
|
||||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
|
||||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
|
||||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
|
||||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
|
||||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
|
||||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
|
||||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
|
||||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
|
||||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
|
||||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
|
||||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
|
||||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
|
||||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
|
||||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
|
||||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
|
||||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
|
||||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
|
||||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
|
||||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
|
||||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
|
||||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
|
||||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
|
||||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
|
||||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
|
||||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
|
||||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
|
||||||
# via -r requirements.in
|
|
||||||
sqlalchemy==2.0.45 \
|
|
||||||
--hash=sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7 \
|
|
||||||
--hash=sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826 \
|
|
||||||
--hash=sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953 \
|
|
||||||
--hash=sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0 \
|
|
||||||
--hash=sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6 \
|
|
||||||
--hash=sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac \
|
|
||||||
--hash=sha256:13e27397a7810163440c6bfed6b3fe46f1bfb2486eb540315a819abd2c004128 \
|
|
||||||
--hash=sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88 \
|
|
||||||
--hash=sha256:1d8b4a7a8c9b537509d56d5cd10ecdcfbb95912d72480c8861524efecc6a3fff \
|
|
||||||
--hash=sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4 \
|
|
||||||
--hash=sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33 \
|
|
||||||
--hash=sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56 \
|
|
||||||
--hash=sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177 \
|
|
||||||
--hash=sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b \
|
|
||||||
--hash=sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177 \
|
|
||||||
--hash=sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a \
|
|
||||||
--hash=sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0 \
|
|
||||||
--hash=sha256:56ead1f8dfb91a54a28cd1d072c74b3d635bcffbd25e50786533b822d4f2cde2 \
|
|
||||||
--hash=sha256:5964f832431b7cdfaaa22a660b4c7eb1dfcd6ed41375f67fd3e3440fd95cb3cc \
|
|
||||||
--hash=sha256:59a8b8bd9c6bedf81ad07c8bd5543eedca55fe9b8780b2b628d495ba55f8db1e \
|
|
||||||
--hash=sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e \
|
|
||||||
--hash=sha256:6d0beadc2535157070c9c17ecf25ecec31e13c229a8f69196d7590bde8082bf1 \
|
|
||||||
--hash=sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4 \
|
|
||||||
--hash=sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774 \
|
|
||||||
--hash=sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a \
|
|
||||||
--hash=sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6 \
|
|
||||||
--hash=sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce \
|
|
||||||
--hash=sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74 \
|
|
||||||
--hash=sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1 \
|
|
||||||
--hash=sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b \
|
|
||||||
--hash=sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8 \
|
|
||||||
--hash=sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2 \
|
|
||||||
--hash=sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee \
|
|
||||||
--hash=sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f \
|
|
||||||
--hash=sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b \
|
|
||||||
--hash=sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d \
|
|
||||||
--hash=sha256:c1c2091b1489435ff85728fafeb990f073e64f6f5e81d5cd53059773e8521eb6 \
|
|
||||||
--hash=sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85 \
|
|
||||||
--hash=sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b \
|
|
||||||
--hash=sha256:d29b2b99d527dbc66dd87c3c3248a5dd789d974a507f4653c969999fc7c1191b \
|
|
||||||
--hash=sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0 \
|
|
||||||
--hash=sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c \
|
|
||||||
--hash=sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a \
|
|
||||||
--hash=sha256:db6834900338fb13a9123307f0c2cbb1f890a8656fcd5e5448ae3ad5bbe8d312 \
|
|
||||||
--hash=sha256:e057f928ffe9c9b246a55b469c133b98a426297e1772ad24ce9f0c47d123bd5b \
|
|
||||||
--hash=sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f \
|
|
||||||
--hash=sha256:ebd300afd2b62679203435f596b2601adafe546cb7282d5a0cd3ed99e423720f \
|
|
||||||
--hash=sha256:ed3635353e55d28e7f4a95c8eda98a5cdc0a0b40b528433fbd41a9ae88f55b3d \
|
|
||||||
--hash=sha256:ee580ab50e748208754ae8980cec79ec205983d8cf8b3f7c39067f3d9f2c8e22 \
|
|
||||||
--hash=sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606 \
|
|
||||||
--hash=sha256:fd93c6f5d65f254ceabe97548c709e073d6da9883343adaa51bf1a913ce93f8e \
|
|
||||||
--hash=sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf
|
|
||||||
# via sqlmodel
|
|
||||||
sqlmodel==0.0.31 \
|
|
||||||
--hash=sha256:2d41a8a9ee05e40736e2f9db8ea28cbfe9b5d4e5a18dd139e80605025e0c516c \
|
|
||||||
--hash=sha256:6d946d56cac4c2db296ba1541357cee2e795d68174e2043cd138b916794b1513
|
|
||||||
# via -r requirements.in
|
|
||||||
starlette==0.50.0 \
|
|
||||||
--hash=sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca \
|
|
||||||
--hash=sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca
|
|
||||||
# via fastapi
|
|
||||||
typing-extensions==4.15.0 \
|
|
||||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
|
||||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
|
||||||
# via
|
|
||||||
# anyio
|
|
||||||
# fastapi
|
|
||||||
# pydantic
|
|
||||||
# pydantic-core
|
|
||||||
# pytest-asyncio
|
|
||||||
# sqlalchemy
|
|
||||||
# starlette
|
|
||||||
# typing-inspection
|
|
||||||
typing-inspection==0.4.2 \
|
|
||||||
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
|
|
||||||
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
|
|
||||||
# via
|
|
||||||
# pydantic
|
|
||||||
# pydantic-settings
|
|
||||||
uvicorn==0.40.0 \
|
|
||||||
--hash=sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea \
|
|
||||||
--hash=sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee
|
|
||||||
# via -r requirements.in
|
|
||||||
15
helper/home_automation_backend_template.conf
Normal file
15
helper/home_automation_backend_template.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[program:home_automation_backend]
|
||||||
|
command=
|
||||||
|
directory=
|
||||||
|
user=
|
||||||
|
group=
|
||||||
|
environment=
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
startsecs=15
|
||||||
|
startretries=100
|
||||||
|
stopwaitsecs=30
|
||||||
|
redirect_stderr=true
|
||||||
|
stdout_logfile=/var/log/supervisor/%(program_name)s.log
|
||||||
|
stdout_logfile_maxbytes=5MB
|
||||||
|
stdout_logfile_backups=5
|
||||||
100
helper/install.sh
Executable file
100
helper/install.sh
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/bash
|
||||||
|
|
||||||
|
# Argument parsing
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 [--install|--uninstall|--help]"
|
||||||
|
echo " --install Install the automation backend"
|
||||||
|
echo " --uninstall Uninstall the automation backend"
|
||||||
|
echo " --update Update the installation"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
key="$1"
|
||||||
|
case $key in
|
||||||
|
--install)
|
||||||
|
INSTALL=true
|
||||||
|
;;
|
||||||
|
--uninstall)
|
||||||
|
UNINSTALL=true
|
||||||
|
;;
|
||||||
|
--update)
|
||||||
|
UPDATE=true
|
||||||
|
;;
|
||||||
|
--help)
|
||||||
|
echo "Usage: $0 [--install|--uninstall|--update|--help]"
|
||||||
|
echo " --install Install the automation backend"
|
||||||
|
echo " --uninstall Uninstall the automation backend"
|
||||||
|
echo " --update Update the installation"
|
||||||
|
echo " --help Show this help message"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid argument: $key"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
TARGET_DIR="$HOME/.local/home-automation-backend"
|
||||||
|
SUPERVISOR_CFG_NAME="home_automation_backend"
|
||||||
|
APP_NAME="home-automation-backend"
|
||||||
|
SUPERVISOR_CFG="$SUPERVISOR_CFG_NAME.conf"
|
||||||
|
BASEDIR=$(dirname "$(realpath "$0")")
|
||||||
|
|
||||||
|
# Install or uninstall based on arguments
|
||||||
|
install_backend() {
|
||||||
|
# Installation code here
|
||||||
|
echo "Installing..."
|
||||||
|
|
||||||
|
sudo supervisorctl stop $SUPERVISOR_CFG_NAME
|
||||||
|
|
||||||
|
mkdir -p $TARGET_DIR
|
||||||
|
cd $BASEDIR"/../src/" && go build -o $TARGET_DIR/$APP_NAME
|
||||||
|
|
||||||
|
|
||||||
|
cp $BASEDIR/"$SUPERVISOR_CFG_NAME"_template.conf $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
|
||||||
|
sed -i "s+command=+command=$TARGET_DIR/$APP_NAME serve+g" $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
sed -i "s+directory=+directory=$TARGET_DIR+g" $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
sed -i "s+user=+user=$USER+g" $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
sed -i "s+group=+group=$USER+g" $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
sed -i "s+environment=+environment=HOME=\"$HOME\"+g" $BASEDIR/$SUPERVISOR_CFG
|
||||||
|
|
||||||
|
sudo mv $BASEDIR/$SUPERVISOR_CFG /etc/supervisor/conf.d/$SUPERVISOR_CFG
|
||||||
|
|
||||||
|
sudo supervisorctl reread
|
||||||
|
sudo supervisorctl update
|
||||||
|
sudo supervisorctl start $SUPERVISOR_CFG_NAME
|
||||||
|
|
||||||
|
echo "Installation complete."
|
||||||
|
}
|
||||||
|
uninstall_backend() {
|
||||||
|
# Uninstallation code here
|
||||||
|
echo "Uninstalling..."
|
||||||
|
|
||||||
|
sudo supervisorctl stop $SUPERVISOR_CFG_NAME
|
||||||
|
|
||||||
|
sudo supervisorctl remove $SUPERVISOR_CFG_NAME
|
||||||
|
|
||||||
|
sudo rm /etc/supervisor/conf.d/$SUPERVISOR_CFG
|
||||||
|
|
||||||
|
rm -rf $TARGET_DIR/
|
||||||
|
|
||||||
|
echo "Uninstallation complete."
|
||||||
|
echo "Config files and db is stored in $HOME/.config/home-automation"
|
||||||
|
}
|
||||||
|
update_backend() {
|
||||||
|
uninstall_backend
|
||||||
|
install_backend
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $INSTALL ]]; then
|
||||||
|
install_backend
|
||||||
|
elif [[ $UNINSTALL ]]; then
|
||||||
|
uninstall_backend
|
||||||
|
elif [[ $UPDATE ]]; then
|
||||||
|
update_backend
|
||||||
|
else
|
||||||
|
echo "Invalid argument: $key"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "home-automation-backend"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[tool.setuptools]
|
|
||||||
packages = ["app"]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
fastapi
|
|
||||||
uvicorn
|
|
||||||
httpx
|
|
||||||
pyyaml
|
|
||||||
pydantic-settings
|
|
||||||
sqlmodel
|
|
||||||
argon2-cffi
|
|
||||||
26
ruff.toml
26
ruff.toml
@@ -1,26 +0,0 @@
|
|||||||
target-version = "py39"
|
|
||||||
line-length = 144
|
|
||||||
|
|
||||||
[lint]
|
|
||||||
select = ["ALL"]
|
|
||||||
fixable = ["UP034", "I001"]
|
|
||||||
ignore = [
|
|
||||||
"T201",
|
|
||||||
"D",
|
|
||||||
"ANN101",
|
|
||||||
"TD002",
|
|
||||||
"TD003",
|
|
||||||
"TRY003",
|
|
||||||
"EM101",
|
|
||||||
"EM102",
|
|
||||||
"SIM108",
|
|
||||||
"C901",
|
|
||||||
"PLR0912",
|
|
||||||
"PLR0915",
|
|
||||||
"PLR0913",
|
|
||||||
"PLC0415",
|
|
||||||
]
|
|
||||||
|
|
||||||
[lint.extend-per-file-ignores]
|
|
||||||
"test*.py" = ["S101", "S105", "S106", "PT011", "PLR2004"]
|
|
||||||
"models*.py" = ["FA102"]
|
|
||||||
41
src/cmd/root.go
Normal file
41
src/cmd/root.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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: "home-automation-backend",
|
||||||
|
Short: "This is the entry point of the home automation backend",
|
||||||
|
Long: `Home automation backend is a RESTful API server that provides
|
||||||
|
automation features for may devices.`,
|
||||||
|
// 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/.home-automation-backend.yaml)")
|
||||||
|
|
||||||
|
// Cobra also supports local flags, which will only run
|
||||||
|
// when this action is called directly.
|
||||||
|
}
|
||||||
160
src/cmd/serve.go
Normal file
160
src/cmd/serve.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2024 Tianyu Liu
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/t-liu93/home-automation-backend/components/homeassistant"
|
||||||
|
"github.com/t-liu93/home-automation-backend/components/locationRecorder"
|
||||||
|
"github.com/t-liu93/home-automation-backend/components/pooRecorder"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/notion"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/ticktickutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
port string
|
||||||
|
scheduler gocron.Scheduler
|
||||||
|
ticktick *ticktickutil.TicktickUtilImpl
|
||||||
|
)
|
||||||
|
|
||||||
|
// serveCmd represents the serve command
|
||||||
|
var serveCmd = &cobra.Command{
|
||||||
|
Use: "serve",
|
||||||
|
Short: "Server automation backend",
|
||||||
|
Run: serve,
|
||||||
|
}
|
||||||
|
|
||||||
|
func initUtil() {
|
||||||
|
// init notion
|
||||||
|
if viper.InConfig("notion.token") {
|
||||||
|
notion.Init(viper.GetString("notion.token"))
|
||||||
|
} else {
|
||||||
|
slog.Error("Notion token not found in config file, exiting..")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// init ticktick
|
||||||
|
ticktick = ticktickutil.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initComponent() {
|
||||||
|
// init pooRecorder
|
||||||
|
pooRecorder.Init(&scheduler)
|
||||||
|
// init location recorder
|
||||||
|
locationRecorder.Init()
|
||||||
|
// init homeassistant
|
||||||
|
homeassistant.Init(ticktick)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve(cmd *cobra.Command, args []string) {
|
||||||
|
slog.Info("Starting server..")
|
||||||
|
|
||||||
|
viper.SetConfigName("config") // name of config file (without extension)
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.AddConfigPath(".") // . is used for dev
|
||||||
|
viper.AddConfigPath("$HOME/.config/home-automation")
|
||||||
|
err := viper.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Cannot read config file, %s, exiting..", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
viper.WatchConfig()
|
||||||
|
viper.SetDefault("logLevel", "info")
|
||||||
|
logLevelCfg := viper.GetString("logLevel")
|
||||||
|
switch logLevelCfg {
|
||||||
|
case "debug":
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||||
|
case "info":
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelInfo)
|
||||||
|
case "warn":
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelWarn)
|
||||||
|
case "error":
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.InConfig("port") {
|
||||||
|
port = viper.GetString("port")
|
||||||
|
} else {
|
||||||
|
slog.Error("Port not found in config file, exiting..")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
scheduler, err = gocron.NewScheduler()
|
||||||
|
defer scheduler.Shutdown()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Cannot create scheduler, %s, exiting..", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
initUtil()
|
||||||
|
initComponent()
|
||||||
|
scheduler.Start()
|
||||||
|
|
||||||
|
// routing
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("OK"))
|
||||||
|
}).Methods("GET")
|
||||||
|
|
||||||
|
router.HandleFunc("/poo/latest", pooRecorder.HandleNotifyLatestPoo).Methods("GET")
|
||||||
|
router.HandleFunc("/poo/record", pooRecorder.HandleRecordPoo).Methods("POST")
|
||||||
|
router.HandleFunc("/homeassistant/publish", homeassistant.HandleHaMessage).Methods("POST")
|
||||||
|
|
||||||
|
router.HandleFunc("/location/record", locationRecorder.HandleRecordLocation).Methods("POST")
|
||||||
|
|
||||||
|
router.HandleFunc("/ticktick/auth/code", ticktick.HandleAuthCode).Methods("GET")
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":" + port,
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
slog.Error(fmt.Sprintf("ListenAndServe error: %v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintln("Server started on port", port))
|
||||||
|
|
||||||
|
<-stop
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintln("Shutting down the server..."))
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Server Shutdown Failed:%+v", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintln("Server gracefully stopped"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(serveCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// serveCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// serveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
serveCmd.Flags().StringVarP(&port, "port", "p", "18881", "Port to listen on")
|
||||||
|
}
|
||||||
147
src/components/homeassistant/homeassistant.go
Normal file
147
src/components/homeassistant/homeassistant.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package homeassistant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/ticktickutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type haMessage struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionTask struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
DueHour int `json:"due_hour"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ticktickUtil ticktickutil.TicktickUtil
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(ticktick ticktickutil.TicktickUtil) {
|
||||||
|
ticktickUtil = ticktick
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleHaMessage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var message haMessage
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&message)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleHaMessage: Error decoding request body", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch message.Target {
|
||||||
|
case "poo_recorder":
|
||||||
|
res := handlePooRecorderMsg(message)
|
||||||
|
if !res {
|
||||||
|
http.Error(w, "Error handling poo recorder message", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
case "location_recorder":
|
||||||
|
res := handleLocationRecorderMsg(message)
|
||||||
|
if !res {
|
||||||
|
http.Error(w, "Error handling location recorder message", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
case "ticktick":
|
||||||
|
res := handleTicktickMsg(message)
|
||||||
|
if !res {
|
||||||
|
http.Error(w, "Error handling ticktick message", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
slog.Warn(fmt.Sprintln("HandleHaMessage: Unknown target", message.Target))
|
||||||
|
http.Error(w, "Unknown target", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePooRecorderMsg(message haMessage) bool {
|
||||||
|
switch message.Action {
|
||||||
|
case "get_latest":
|
||||||
|
return handleGetLatestPoo()
|
||||||
|
default:
|
||||||
|
slog.Warn(fmt.Sprintln("handlePooRecorderMsg: Unknown action", message.Action))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLocationRecorderMsg(message haMessage) bool {
|
||||||
|
if message.Action == "record" {
|
||||||
|
port := viper.GetString("port")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
_, err := client.Post("http://localhost:"+port+"/location/record", "application/json", strings.NewReader(strings.ReplaceAll(message.Content, "'", "\"")))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("handleLocationRecorderMsg: Error sending request to location recorder", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
slog.Warn(fmt.Sprintln("handleLocationRecorderMsg: Unknown action", message.Action))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTicktickMsg(message haMessage) bool {
|
||||||
|
switch message.Action {
|
||||||
|
case "create_action_task":
|
||||||
|
return createActionTask(message)
|
||||||
|
default:
|
||||||
|
slog.Warn(fmt.Sprintln("handleTicktickMsg: Unknown action", message.Action))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetLatestPoo() bool {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
port := viper.GetString("port")
|
||||||
|
_, err := client.Get("http://localhost:" + port + "/poo/latest")
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("handleGetLatestPoo: Error sending request to poo recorder", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func createActionTask(message haMessage) bool {
|
||||||
|
if !viper.IsSet("homeassistant.actionTaskProjectId") {
|
||||||
|
slog.Warn("Homeassistant.createActionTask actionTaskProjectId not set")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
projectId := viper.GetString("homeassistant.actionTaskProjectId")
|
||||||
|
detail := strings.ReplaceAll(message.Content, "'", "\"")
|
||||||
|
var task actionTask
|
||||||
|
err := json.Unmarshal([]byte(detail), &task)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Homeassistant.createActionTask: Error unmarshalling", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
dueHour := task.DueHour
|
||||||
|
due := time.Now().Add(time.Hour * time.Duration(dueHour))
|
||||||
|
dueNextMidnight := time.Date(due.Year(), due.Month(), due.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, 1)
|
||||||
|
dueTicktick := dueNextMidnight.UTC().Format(ticktickutil.DateTimeLayout)
|
||||||
|
ticktickTask := ticktickutil.Task{
|
||||||
|
ProjectId: projectId,
|
||||||
|
Title: task.Action,
|
||||||
|
DueDate: dueTicktick,
|
||||||
|
}
|
||||||
|
err = ticktickUtil.CreateTask(ticktickTask)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("Homeassistant.createActionTask: Error creating task %s", err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
284
src/components/homeassistant/homeassistant_test.go
Normal file
284
src/components/homeassistant/homeassistant_test.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package homeassistant
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/ticktickutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loggerText = new(bytes.Buffer)
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MockedTicktickUtil struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupTearDown(t *testing.T) func() {
|
||||||
|
loggertearDown := loggerSetupTeardown()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
loggertearDown()
|
||||||
|
viper.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggerSetupTeardown() func() {
|
||||||
|
logger := slog.New(slog.NewTextHandler(loggerText, nil))
|
||||||
|
defaultLogger := slog.Default()
|
||||||
|
slog.SetDefault(logger)
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
slog.SetDefault(defaultLogger)
|
||||||
|
loggerText.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockedTicktickUtil) HandleAuthCode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.Called(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockedTicktickUtil) GetTasks(projectId string) []ticktickutil.Task {
|
||||||
|
args := m.Called(projectId)
|
||||||
|
return args.Get(0).([]ticktickutil.Task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockedTicktickUtil) HasDuplicateTask(projectId string, taskTitile string) bool {
|
||||||
|
args := m.Called(projectId, taskTitile)
|
||||||
|
return args.Bool(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockedTicktickUtil) CreateTask(task ticktickutil.Task) error {
|
||||||
|
args := m.Called(task)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleHaMessageJsonDecodeError(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
invalidRequestBody := ` { "target": "poo_recorder", "action": "get_latest", "content": " }`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(invalidRequestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "HandleHaMessage: Error decoding request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlePooRecorderMsgGetLatest(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
requestBody := `{"target": "poo_recorder", "action": "get_latest", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, http.MethodGet, r.Method)
|
||||||
|
assert.Equal(t, "/poo/latest", r.URL.Path)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
port := strings.Split(server.URL, ":")[2]
|
||||||
|
viper.Set("port", port)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Empty(t, loggerText.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlePooRecorderMsgUnknownAction(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "poo_recorder", "action": "unknown_action", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "handlePooRecorderMsg: Unknown action")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlePooRecorderMsgGetLatestError(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "poo_recorder", "action": "get_latest", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
port := "invalid port"
|
||||||
|
viper.Set("port", port)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "handleGetLatestPoo: Error sending request to poo recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLocationRecorderMsg(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "location_recorder", "action": "record", "content": "{'person': 'test', 'latitude': '1.0', 'longitude': '2.0', 'altitude': '3.0'}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, http.MethodPost, r.Method)
|
||||||
|
assert.Equal(t, "/location/record", r.URL.Path)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
port := strings.Split(server.URL, ":")[2]
|
||||||
|
viper.Set("port", port)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Empty(t, loggerText.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLocationRecorderMsgUnknownAction(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "location_recorder", "action": "unknown_action", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "handleLocationRecorderMsg: Unknown action")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleLocationRecorderMsgRequestErr(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "location_recorder", "action": "record", "content": "{'person': 'test', 'latitude': '1.0', 'longitude': '2.0', 'altitude': '3.0'}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
port := "invalid port"
|
||||||
|
viper.Set("port", port)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "handleLocationRecorderMsg: Error sending request to location recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTicktickMsgCreateActionTask(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
const expectedProjectId = "test_project_id"
|
||||||
|
const dueHour = 12
|
||||||
|
due := time.Now().Add(time.Hour * time.Duration(dueHour))
|
||||||
|
dueNextMidnight := time.Date(due.Year(), due.Month(), due.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, 1)
|
||||||
|
dueTicktick := dueNextMidnight.UTC().Format(ticktickutil.DateTimeLayout)
|
||||||
|
|
||||||
|
requestBody := `{"target": "ticktick", "action": "create_action_task", "content": "{'title': 'test', 'action': 'test_action', 'due_hour': 12}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
mockedTicktickUtil := new(MockedTicktickUtil)
|
||||||
|
viper.Set("homeassistant.actionTaskProjectId", expectedProjectId)
|
||||||
|
|
||||||
|
mockedTicktickUtil.On("CreateTask", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
Init(mockedTicktickUtil)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
|
||||||
|
expectedTask := ticktickutil.Task{
|
||||||
|
Title: "test_action",
|
||||||
|
ProjectId: expectedProjectId,
|
||||||
|
DueDate: dueTicktick,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockedTicktickUtil.AssertCalled(t, "CreateTask", expectedTask)
|
||||||
|
mockedTicktickUtil.AssertNumberOfCalls(t, "CreateTask", 1)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Empty(t, loggerText.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTicktickMsgUnknownAction(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "ticktick", "action": "unknown_action", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "handleTicktickMsg: Unknown action")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTicktickMsgProjectIdUnset(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "ticktick", "action": "create_action_task", "content": "{'title': 'test', 'action': 'test_action', 'due_hour': 12}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "Homeassistant.createActionTask actionTaskProjectId not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTicktickMsgJsonError(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
invalidRequestBody := ` { "target": "ticktick", "action": "create_action_task", "content": "{'title': 'tes, 'action': 'test_action', 'due_hour': 12}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(invalidRequestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
viper.Set("homeassistant.actionTaskProjectId", "some project id")
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "Homeassistant.createActionTask: Error unmarshalling")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleTicktickMsgTicktickUtilErr(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "ticktick", "action": "create_action_task", "content": "{'title': 'test', 'action': 'test_action', 'due_hour': 12}"}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
mockedTicktickUtil := new(MockedTicktickUtil)
|
||||||
|
viper.Set("homeassistant.actionTaskProjectId", "some project id")
|
||||||
|
|
||||||
|
mockedTicktickUtil.On("CreateTask", mock.Anything).Return(errors.New("some error"))
|
||||||
|
|
||||||
|
Init(mockedTicktickUtil)
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
|
||||||
|
mockedTicktickUtil.AssertCalled(t, "CreateTask", mock.Anything)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "Homeassistant.createActionTask: Error creating task")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleHaMessageUnknownTarget(t *testing.T) {
|
||||||
|
teardown := SetupTearDown(t)
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
requestBody := `{"target": "unknown_target", "action": "record", "content": ""}`
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/homeassistant/publish", strings.NewReader(requestBody))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
HandleHaMessage(w, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, loggerText.String(), "HandleHaMessage: Unknown target")
|
||||||
|
}
|
||||||
194
src/components/locationRecorder/locationRecorder.go
Normal file
194
src/components/locationRecorder/locationRecorder.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package locationRecorder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
currentDBVersion = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Person string `json:"person"`
|
||||||
|
DateTime string `json:"datetime"`
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
Altitude sql.NullFloat64 `json:"altitude,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationContent struct {
|
||||||
|
Person string `json:"person"`
|
||||||
|
Latitude string `json:"latitude"`
|
||||||
|
Longitude string `json:"longitude"`
|
||||||
|
Altitude string `json:"altitude,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
initDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRecordLocation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var location LocationContent
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&location)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordLocation Error decoding request body", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
latiF64, _ := strconv.ParseFloat(location.Latitude, 64)
|
||||||
|
longiF64, _ := strconv.ParseFloat(location.Longitude, 64)
|
||||||
|
altiF64, _ := strconv.ParseFloat(location.Altitude, 64)
|
||||||
|
InsertLocationNow(location.Person, latiF64, longiF64, altiF64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertLocation(person string, datetime time.Time, latitude float64, longitude float64, altitude float64) {
|
||||||
|
_, err := db.Exec(`INSERT OR IGNORE INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
person, datetime.UTC().Format(time.RFC3339), latitude, longitude, altitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorder.InsertLocation Error inserting location", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertLocationNow(person string, latitude float64, longitude float64, altitude float64) {
|
||||||
|
InsertLocation(person, time.Now(), latitude, longitude, altitude)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDb() {
|
||||||
|
if !viper.InConfig("locationRecorder.dbPath") {
|
||||||
|
slog.Info("LocationRecorderInit dbPath not found in config file, using default: location_recorder.db")
|
||||||
|
viper.SetDefault("locationRecorder.dbPath", "location_recorder.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := viper.GetString("locationRecorder.dbPath")
|
||||||
|
err := error(nil)
|
||||||
|
db, err = sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error opening database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error pinging database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
migrateDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb() {
|
||||||
|
var userVersion int
|
||||||
|
err := db.QueryRow("PRAGMA user_version").Scan(&userVersion)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error getting db user version", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if userVersion == 0 {
|
||||||
|
migrateDb0To1(&userVersion)
|
||||||
|
}
|
||||||
|
if userVersion == 1 {
|
||||||
|
migrateDb1To2(&userVersion)
|
||||||
|
}
|
||||||
|
if userVersion != currentDBVersion {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error unsupported database version", userVersion))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb0To1(userVersion *int) {
|
||||||
|
// this is actually create new db
|
||||||
|
slog.Info("Creating location recorder database version 1..")
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS location (
|
||||||
|
person TEXT NOT NULL,
|
||||||
|
datetime TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
PRIMARY KEY (person, datetime))`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error creating table", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = db.Exec(`PRAGMA user_version = 1`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB0To1 Error setting user version to 1", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*userVersion = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb1To2(userVersion *int) {
|
||||||
|
// this will change the datetime format into Real RFC3339
|
||||||
|
slog.Info("Migrating location recorder database version 1 to 2..")
|
||||||
|
dbTx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit DB1To2 Error beginning transaction", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fail := func(err error, step string) {
|
||||||
|
slog.Error(fmt.Sprintf("LocationRecorderInit DB1To2 Error %s: %s", step, err))
|
||||||
|
dbTx.Rollback()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`ALTER TABLE location RENAME TO location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "renaming table")
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`CREATE TABLE IF NOT EXISTS location (
|
||||||
|
person TEXT NOT NULL,
|
||||||
|
datetime TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
altitude REAL,
|
||||||
|
PRIMARY KEY (person, datetime))`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "creating new table")
|
||||||
|
}
|
||||||
|
row, err := dbTx.Query(`SELECT person, datetime, latitude, longitude, altitude FROM location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "selecting from old table")
|
||||||
|
}
|
||||||
|
defer row.Close()
|
||||||
|
for row.Next() {
|
||||||
|
var location Location
|
||||||
|
err = row.Scan(&location.Person, &location.DateTime, &location.Latitude, &location.Longitude, &location.Altitude)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "scanning row")
|
||||||
|
}
|
||||||
|
dateTime, err := time.Parse("2006-01-02T15:04:05-0700", location.DateTime)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "parsing datetime")
|
||||||
|
}
|
||||||
|
_, err = dbTx.Exec(`INSERT INTO location (person, datetime, latitude, longitude, altitude) VALUES (?, ?, ?, ?, ?)`, location.Person, dateTime.UTC().Format(time.RFC3339), location.Latitude, location.Longitude, location.Altitude)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "inserting new row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(`DROP TABLE location_old`)
|
||||||
|
if err != nil {
|
||||||
|
fail(err, "dropping old table")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbTx.Exec(`PRAGMA user_version = 2`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("LocationRecorderInit Error setting user version to 2", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dbTx.Commit()
|
||||||
|
*userVersion = 2
|
||||||
|
}
|
||||||
366
src/components/pooRecorder/pooRecorder.go
Normal file
366
src/components/pooRecorder/pooRecorder.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package pooRecorder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron/v2"
|
||||||
|
"github.com/jomei/notionapi"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/homeassistantutil"
|
||||||
|
"github.com/t-liu93/home-automation-backend/util/notion"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
db *sql.DB
|
||||||
|
scheduler *gocron.Scheduler
|
||||||
|
)
|
||||||
|
|
||||||
|
type recordDetail struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Latitude string `json:"latitude"`
|
||||||
|
Longitude string `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooStatusSensorAttributes struct {
|
||||||
|
LastPoo string `json:"last_poo"`
|
||||||
|
FriendlyName string `json:"friendly_name,"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooStatusWebhookBody struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooStatusDbEntry struct {
|
||||||
|
Timestamp string
|
||||||
|
Status string
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(mainScheduler *gocron.Scheduler) {
|
||||||
|
initDb()
|
||||||
|
initScheduler(mainScheduler)
|
||||||
|
notionDbSync()
|
||||||
|
publishLatestPooSensor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleRecordPoo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var record recordDetail
|
||||||
|
if !viper.InConfig("pooRecorder.tableId") {
|
||||||
|
slog.Warn("HandleRecordPoo Table ID not found in config file")
|
||||||
|
http.Error(w, "Table ID not found in config file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
err := decoder.Decode(&record)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordPoo Error decoding request body", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
err = storeStatus(record, now)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordPoo Error storing status", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
publishLatestPooSensor()
|
||||||
|
if viper.InConfig("pooRecorder.webhookId") {
|
||||||
|
homeassistantutil.TriggerWebhook(viper.GetString("pooRecorder.webhookId"), pooStatusWebhookBody{Status: record.Status})
|
||||||
|
} else {
|
||||||
|
slog.Warn("HandleRecordPoo Webhook ID not found in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleNotifyLatestPoo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := publishLatestPooSensor()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleNotifyLatestPoo Error publishing latest poo", err))
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Debug(fmt.Sprintln("HandleGetLatestPoo Latest poo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishLatestPooSensor() error {
|
||||||
|
var latest pooStatusDbEntry
|
||||||
|
err := db.QueryRow(`SELECT timestamp, status, latitude, longitude FROM poo_records ORDER BY timestamp DESC LIMIT 1`).Scan(&latest.Timestamp, &latest.Status, &latest.Latitude, &latest.Longitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error getting latest poo", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
recordTime, err := time.Parse("2006-01-02T15:04Z07:00", latest.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleGetLatestPoo Error parsing timestamp", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
viper.SetDefault("pooRecorder.sensorEntityName", "sensor.test_poo_status")
|
||||||
|
viper.SetDefault("pooRecorder.sensorFriendlyName", "Poo Status")
|
||||||
|
sensorEntityName := viper.GetString("pooRecorder.sensorEntityName")
|
||||||
|
sensorFriendlyName := viper.GetString("pooRecorder.sensorFriendlyName")
|
||||||
|
recordTime = recordTime.Local()
|
||||||
|
pooStatus := homeassistantutil.HttpSensor{
|
||||||
|
EntityId: sensorEntityName,
|
||||||
|
State: latest.Status,
|
||||||
|
Attributes: pooStatusSensorAttributes{
|
||||||
|
LastPoo: recordTime.Format("Mon | 2006-01-02 | 15:04"),
|
||||||
|
FriendlyName: sensorFriendlyName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
homeassistantutil.PublishSensor(pooStatus)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDb() {
|
||||||
|
if !viper.InConfig("pooRecorder.dbPath") {
|
||||||
|
slog.Info("PooRecorderInit dbPath not found in config file, using default: pooRecorder.db")
|
||||||
|
viper.SetDefault("pooRecorder.dbPath", "pooRecorder.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := viper.GetString("pooRecorder.dbPath")
|
||||||
|
err := error(nil)
|
||||||
|
db, err = sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error opening database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error pinging database", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
migrateDb()
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb() {
|
||||||
|
var userVersion int
|
||||||
|
err := db.QueryRow("PRAGMA user_version").Scan(&userVersion)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error getting db user version", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if userVersion == 0 {
|
||||||
|
migrateDb0To1(&userVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateDb0To1(userVersion *int) {
|
||||||
|
// this is actually create new db
|
||||||
|
slog.Info("Creating database version 1..")
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS poo_records (
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
latitude REAL NOT NULL,
|
||||||
|
longitude REAL NOT NULL,
|
||||||
|
PRIMARY KEY (timestamp))`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error creating table", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
_, err = db.Exec(`PRAGMA user_version = 1`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error setting user version to 1", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*userVersion = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func initScheduler(mainScheduler *gocron.Scheduler) {
|
||||||
|
scheduler = mainScheduler
|
||||||
|
_, err := (*scheduler).NewJob(gocron.CronJob("0 5 * * *", false), gocron.NewTask(
|
||||||
|
notionDbSync,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderInit Error creating scheduled task", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notionDbSync() {
|
||||||
|
slog.Info("PooRecorder Running DB sync with Notion..")
|
||||||
|
if !viper.InConfig("pooRecorder.tableId") {
|
||||||
|
slog.Warn("PooRecorder Table ID not found in config file, sync aborted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tableId := viper.GetString("pooRecorder.tableId")
|
||||||
|
rowsNotion, err := notion.GetAllTableRows(tableId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to get table header", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header := rowsNotion[0]
|
||||||
|
rowsNotion = rowsNotion[1:] // remove header
|
||||||
|
rowsDb, err := db.Query(`SELECT * FROM poo_records`)
|
||||||
|
rowsDbMap := make(map[string]pooStatusDbEntry)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to get db rows", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rowsDb.Close()
|
||||||
|
for rowsDb.Next() {
|
||||||
|
var row pooStatusDbEntry
|
||||||
|
err = rowsDb.Scan(&row.Timestamp, &row.Status, &row.Latitude, &row.Longitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to scan db row", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rowsDbMap[row.Timestamp] = row
|
||||||
|
}
|
||||||
|
// notion to db
|
||||||
|
syncNotionToDb(rowsNotion, rowsDbMap)
|
||||||
|
|
||||||
|
// db to notion
|
||||||
|
syncDbToNotion(header.GetID().String(), tableId, rowsNotion)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncNotionToDb(rowsNotion []notionapi.TableRowBlock, rowsDbMap map[string]pooStatusDbEntry) {
|
||||||
|
counter := 0
|
||||||
|
for _, rowNotion := range rowsNotion {
|
||||||
|
rowNotionTimestamp := rowNotion.TableRow.Cells[0][0].PlainText + "T" + rowNotion.TableRow.Cells[1][0].PlainText
|
||||||
|
rowNotionTime, err := time.ParseInLocation("2006-01-02T15:04", rowNotionTimestamp, time.Now().Location())
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to parse timestamp", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rowNotionTimeInDbFormat := rowNotionTime.UTC().Format("2006-01-02T15:04Z07:00")
|
||||||
|
_, exists := rowsDbMap[rowNotionTimeInDbFormat]
|
||||||
|
if !exists {
|
||||||
|
locationNotion := rowNotion.TableRow.Cells[3][0].PlainText
|
||||||
|
latitude, err := strconv.ParseFloat(strings.Split(locationNotion, ",")[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to parse latitude to float", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
longitude, err := strconv.ParseFloat(strings.Split(locationNotion, ",")[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to parse longitude to float", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = db.Exec(`INSERT INTO poo_records (timestamp, status, latitude, longitude) VALUES (?, ?, ?, ?)`,
|
||||||
|
rowNotionTimeInDbFormat, rowNotion.TableRow.Cells[2][0].PlainText, latitude, longitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to insert new row", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintln("PooRecorderSyncDb Inserted", counter, "new rows from Notion to DB"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDbToNotion(headerId string, tableId string, rowsNotion []notionapi.TableRowBlock) {
|
||||||
|
counter := 0
|
||||||
|
var rowsDbSlice []pooStatusDbEntry
|
||||||
|
rowsDb, err := db.Query(`SELECT * FROM poo_records ORDER BY timestamp DESC`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to get db rows", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rowsDb.Close()
|
||||||
|
for rowsDb.Next() {
|
||||||
|
var row pooStatusDbEntry
|
||||||
|
err = rowsDb.Scan(&row.Timestamp, &row.Status, &row.Latitude, &row.Longitude)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("PooRecorderSyncDb Failed to scan db row", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rowsDbSlice = append(rowsDbSlice, row)
|
||||||
|
}
|
||||||
|
startFromId := headerId
|
||||||
|
for iNotion, iDb := 0, 0; iNotion < len(rowsNotion) && iDb < len(rowsDbSlice); {
|
||||||
|
notionTimeStamp := rowsNotion[iNotion].TableRow.Cells[0][0].PlainText + "T" + rowsNotion[iNotion].TableRow.Cells[1][0].PlainText
|
||||||
|
notionTime, err := time.ParseInLocation("2006-01-02T15:04", notionTimeStamp, time.Now().Location())
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to parse notion timestamp", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notionTimeStampInDbFormat := notionTime.UTC().Format("2006-01-02T15:04Z07:00")
|
||||||
|
dbTimeStamp := rowsDbSlice[iDb].Timestamp
|
||||||
|
dbTime, err := time.Parse("2006-01-02T15:04Z07:00", dbTimeStamp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to parse db timestamp", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dbTimeLocal := dbTime.Local()
|
||||||
|
dbTimeDate := dbTimeLocal.Format("2006-01-02")
|
||||||
|
dbTimeTime := dbTimeLocal.Format("15:04")
|
||||||
|
if notionTimeStampInDbFormat == dbTimeStamp {
|
||||||
|
startFromId = rowsNotion[iNotion].GetID().String()
|
||||||
|
iNotion++
|
||||||
|
iDb++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if iNotion != len(rowsNotion)-1 {
|
||||||
|
notionNextTimeStamp := rowsNotion[iNotion+1].TableRow.Cells[0][0].PlainText + "T" + rowsNotion[iNotion+1].TableRow.Cells[1][0].PlainText
|
||||||
|
notionNextTime, err := time.ParseInLocation("2006-01-02T15:04", notionNextTimeStamp, time.Now().Location())
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to parse next notion timestamp", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if notionNextTime.After(notionTime) {
|
||||||
|
slog.Error(fmt.Sprintf("PooRecorderSyncDb Notion timestamp %s is after next timestamp %s, checking, aborting", notionTimeStamp, notionNextTimeStamp))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id, err := notion.WriteTableRow([]string{
|
||||||
|
dbTimeDate,
|
||||||
|
dbTimeTime,
|
||||||
|
rowsDbSlice[iDb].Status,
|
||||||
|
fmt.Sprintf("%s,%s",
|
||||||
|
strconv.FormatFloat(rowsDbSlice[iDb].Latitude, 'f', -1, 64),
|
||||||
|
strconv.FormatFloat(rowsDbSlice[iDb].Longitude, 'f', -1, 64))},
|
||||||
|
tableId,
|
||||||
|
startFromId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PooRecorderSyncDb Failed to write row to Notion", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startFromId = id
|
||||||
|
iDb++
|
||||||
|
counter++
|
||||||
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintln("PooRecorderSyncDb Inserted", counter, "new rows from DB to Notion"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeStatus(record recordDetail, timestamp time.Time) error {
|
||||||
|
tableId := viper.GetString("pooRecorder.tableId")
|
||||||
|
recordDate := timestamp.Format("2006-01-02")
|
||||||
|
recordTime := timestamp.Format("15:04")
|
||||||
|
slog.Debug(fmt.Sprintln("Recording poo", record.Status, "at", record.Latitude, record.Longitude))
|
||||||
|
_, err := db.Exec(`INSERT OR IGNORE INTO poo_records (timestamp, status, latitude, longitude) VALUES (?, ?, ?, ?)`,
|
||||||
|
timestamp.UTC().Format("2006-01-02T15:04Z07:00"), record.Status, record.Latitude, record.Longitude)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
header, err := notion.GetTableRows(tableId, 1, "")
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to get table header", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(header) == 0 {
|
||||||
|
slog.Warn("HandleRecordPoo Table header not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
headerId := header[0].GetID()
|
||||||
|
_, err = notion.WriteTableRow([]string{recordDate, recordTime, record.Status, record.Latitude + "," + record.Longitude}, tableId, headerId.String())
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleRecordPoo Failed to write table row", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
src/go.mod
Normal file
54
src/go.mod
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
module github.com/t-liu93/home-automation-backend
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-co-op/gocron/v2 v2.11.0
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
github.com/jomei/notionapi v1.13.2
|
||||||
|
github.com/spf13/cobra v1.8.1
|
||||||
|
github.com/spf13/viper v1.19.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/term v0.24.0
|
||||||
|
modernc.org/sqlite v1.33.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
modernc.org/libc v1.55.3 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.8.0 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
|
)
|
||||||
139
src/go.sum
Normal file
139
src/go.sum
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||||
|
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
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/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
|
||||||
|
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||||
|
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||||
|
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
|
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
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=
|
||||||
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
|
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||||
|
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||||
|
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||||
|
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||||
|
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
|
||||||
|
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
|
||||||
|
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||||
|
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||||
|
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||||
|
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
|
||||||
|
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||||
|
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||||
|
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||||
|
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
|
||||||
|
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
0
src/helper/location_recorder/LICENSE
Normal file
0
src/helper/location_recorder/LICENSE
Normal file
40
src/helper/location_recorder/cmd/addgpx.go
Normal file
40
src/helper/location_recorder/cmd/addgpx.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2024 Tianyu Liu
|
||||||
|
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// addgpxCmd represents the addgpx command
|
||||||
|
var addgpxCmd = &cobra.Command{
|
||||||
|
Use: "addgpx",
|
||||||
|
Short: "A brief description of your command",
|
||||||
|
Long: `A longer description that spans multiple lines and likely contains examples
|
||||||
|
and usage of using your command. 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.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Println("addgpx called")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(addgpxCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// addgpxCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// addgpxCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
51
src/helper/location_recorder/cmd/root.go
Normal file
51
src/helper/location_recorder/cmd/root.go
Normal file
@@ -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: "location_recorder",
|
||||||
|
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/.location_recorder.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")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
11
src/helper/location_recorder/main.go
Normal file
11
src/helper/location_recorder/main.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2024 Tianyu Liu
|
||||||
|
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/t-liu93/home-automation-backend/helper/location_recorder/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
0
src/helper/poo_recorder_helper/LICENSE
Normal file
0
src/helper/poo_recorder_helper/LICENSE
Normal file
127
src/helper/poo_recorder_helper/cmd/reverse.go
Normal file
127
src/helper/poo_recorder_helper/cmd/reverse.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2024 Tianyu Liu
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jomei/notionapi"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var notionToken string
|
||||||
|
var notionTableId string
|
||||||
|
|
||||||
|
// reverseCmd represents the reverse command
|
||||||
|
var reverseCmd = &cobra.Command{
|
||||||
|
Use: "reverse",
|
||||||
|
Short: "Reverse given poo recording table",
|
||||||
|
Long: `Reverse the given poo recording table. Provide the Notion API token and the table ID to reverse.
|
||||||
|
The Notion API token can be obtained from https://www.notion.so/my-integrations. The table ID can be obtained from the URL of the table.
|
||||||
|
The token and table ID will be input in the following prompt.
|
||||||
|
`,
|
||||||
|
Run: readCredentials,
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCredentials(cmd *cobra.Command, args []string) {
|
||||||
|
if notionToken == "" || notionTableId == "" {
|
||||||
|
fmt.Print("Enter Notion API token: ")
|
||||||
|
pw, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read NOTION API Token: %v", err)
|
||||||
|
}
|
||||||
|
notionToken = string(pw)
|
||||||
|
fmt.Print("\nEnter Notion table ID: ")
|
||||||
|
tableId, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read NOTION table ID: %v", err)
|
||||||
|
}
|
||||||
|
notionTableId = string(tableId)
|
||||||
|
}
|
||||||
|
reverseRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
func reverseRun() {
|
||||||
|
client := notionapi.NewClient(notionapi.Token(notionToken))
|
||||||
|
rows := []notionapi.Block{}
|
||||||
|
fmt.Println("Reverse table ID: ", notionTableId)
|
||||||
|
block, err := client.Block.Get(context.Background(), notionapi.BlockID(notionTableId))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get table detail: %v", err)
|
||||||
|
}
|
||||||
|
if block.GetType().String() != "table" {
|
||||||
|
log.Fatalf("Block ID %s is not a table", notionTableId)
|
||||||
|
}
|
||||||
|
headerBlock, _ := client.Block.GetChildren(context.Background(), notionapi.BlockID(notionTableId), ¬ionapi.Pagination{
|
||||||
|
StartCursor: "",
|
||||||
|
PageSize: 100,
|
||||||
|
})
|
||||||
|
headerId := headerBlock.Results[0].GetID()
|
||||||
|
nextCursor := headerId.String()
|
||||||
|
hasMore := true
|
||||||
|
for hasMore {
|
||||||
|
blockChildren, _ := client.Block.GetChildren(context.Background(), notionapi.BlockID(notionTableId), ¬ionapi.Pagination{
|
||||||
|
StartCursor: notionapi.Cursor(nextCursor),
|
||||||
|
PageSize: 100,
|
||||||
|
})
|
||||||
|
rows = append(rows, blockChildren.Results...)
|
||||||
|
hasMore = blockChildren.HasMore
|
||||||
|
nextCursor = blockChildren.NextCursor
|
||||||
|
}
|
||||||
|
rows = rows[1:]
|
||||||
|
rowsR := reverseTable(rows)
|
||||||
|
nrRowsToDelete := len(rowsR)
|
||||||
|
for index, row := range rowsR {
|
||||||
|
client.Block.Delete(context.Background(), row.GetID())
|
||||||
|
if index%10 == 0 || index == nrRowsToDelete-1 {
|
||||||
|
fmt.Printf("Deleted %d/%d rows\n", index, nrRowsToDelete)
|
||||||
|
}
|
||||||
|
time.Sleep(400 * time.Millisecond)
|
||||||
|
}
|
||||||
|
after := headerId
|
||||||
|
fmt.Println("Writing rows back to table")
|
||||||
|
for len(rowsR) > 0 {
|
||||||
|
var rowsToWrite []notionapi.Block
|
||||||
|
if len(rowsR) > 100 {
|
||||||
|
rowsToWrite = rowsR[:100]
|
||||||
|
} else {
|
||||||
|
rowsToWrite = rowsR
|
||||||
|
}
|
||||||
|
client.Block.AppendChildren(context.Background(), notionapi.BlockID(notionTableId), ¬ionapi.AppendBlockChildrenRequest{
|
||||||
|
After: after,
|
||||||
|
Children: rowsToWrite,
|
||||||
|
})
|
||||||
|
after = rowsToWrite[len(rowsToWrite)-1].GetID()
|
||||||
|
rowsR = rowsR[len(rowsToWrite):]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
39
src/helper/poo_recorder_helper/cmd/root.go
Normal file
39
src/helper/poo_recorder_helper/cmd/root.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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: "Poo recorder helper executables.",
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
11
src/helper/poo_recorder_helper/main.go
Normal file
11
src/helper/poo_recorder_helper/main.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
11
src/main.go
Normal file
11
src/main.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2024 Tianyu Liu
|
||||||
|
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/t-liu93/home-automation-backend/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
96
src/util/homeassistantutil/homeassistantutil.go
Normal file
96
src/util/homeassistantutil/homeassistantutil.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package homeassistantutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipField string = "homeassistant.ip"
|
||||||
|
portField string = "homeassistant.port"
|
||||||
|
authTokenField string = "homeassistant.authToken"
|
||||||
|
webhookPath string = "/api/webhook/"
|
||||||
|
sensorPath string = "/api/states/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpSensor struct {
|
||||||
|
EntityId string `json:"entity_id"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Attributes interface{} `json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookBody interface{}
|
||||||
|
|
||||||
|
func TriggerWebhook(webhookId string, body WebhookBody) {
|
||||||
|
if viper.InConfig(ipField) &&
|
||||||
|
viper.InConfig(portField) &&
|
||||||
|
viper.InConfig(authTokenField) {
|
||||||
|
url := fmt.Sprintf("http://%s:%s%s%s", viper.GetString(ipField), viper.GetString(portField), webhookPath, webhookId)
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("TriggerWebhook Error marshalling", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("TriggerWebhook Error creating request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+viper.GetString(authTokenField))
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("TriggerWebhook Error sending request", err))
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
slog.Warn(fmt.Sprintln("TriggerWebhook Unexpected response status", resp.StatusCode))
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
slog.Warn("TriggerWebhook Home Assistant IP, port, or token not found in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PublishSensor(sensor HttpSensor) {
|
||||||
|
if viper.InConfig(ipField) &&
|
||||||
|
viper.InConfig(portField) &&
|
||||||
|
viper.InConfig(authTokenField) {
|
||||||
|
url := fmt.Sprintf("http://%s:%s%s%s", viper.GetString(ipField), viper.GetString(portField), sensorPath, sensor.EntityId)
|
||||||
|
payload, err := json.Marshal(sensor)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PublishSensor Error marshalling", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PublishSensor Error creating request", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+viper.GetString(authTokenField))
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 1,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("PublishSensor Error sending request", err))
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
slog.Warn(fmt.Sprintln("PublishSensor Unexpected response status", resp.StatusCode))
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
} else {
|
||||||
|
slog.Warn("PublishSensor Home Assistant IP, port, or token not found in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/util/notion/notion.go
Normal file
129
src/util/notion/notion.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package notion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/jomei/notionapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
var client *notionapi.Client
|
||||||
|
|
||||||
|
func Init(token string) {
|
||||||
|
client = notionapi.NewClient(notionapi.Token(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClient() *notionapi.Client {
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTableRows(tableId string, numberOfRows int, startFromId string) ([]notionapi.TableRowBlock, error) {
|
||||||
|
if client == nil {
|
||||||
|
return nil, errors.New("notion client not initialized")
|
||||||
|
}
|
||||||
|
var rows []notionapi.TableRowBlock
|
||||||
|
var nextNumberToGet int
|
||||||
|
if numberOfRows > 100 {
|
||||||
|
nextNumberToGet = 100
|
||||||
|
} else {
|
||||||
|
nextNumberToGet = numberOfRows
|
||||||
|
}
|
||||||
|
for numberOfRows > 0 {
|
||||||
|
block, err := client.Block.GetChildren(context.Background(), notionapi.BlockID(tableId), ¬ionapi.Pagination{
|
||||||
|
StartCursor: notionapi.Cursor(startFromId),
|
||||||
|
PageSize: nextNumberToGet,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, block := range block.Results {
|
||||||
|
if block.GetType().String() == "table_row" {
|
||||||
|
tableRow, ok := block.(*notionapi.TableRowBlock)
|
||||||
|
if !ok {
|
||||||
|
slog.Error("Notion.GetTableRows Failed to cast block to table row")
|
||||||
|
return nil, errors.New("Notion.GetTableRows failed to cast block to table row")
|
||||||
|
}
|
||||||
|
rows = append(rows, *tableRow)
|
||||||
|
} else {
|
||||||
|
slog.Error(fmt.Sprintf("Block ID %s is not a table row", block.GetID()))
|
||||||
|
return nil, errors.New("Notion.GetAllTableRows block ID is not a table row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
numberOfRows -= nextNumberToGet
|
||||||
|
if numberOfRows > 100 {
|
||||||
|
nextNumberToGet = 100
|
||||||
|
} else {
|
||||||
|
nextNumberToGet = numberOfRows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTableRows(tableId string) ([]notionapi.TableRowBlock, error) {
|
||||||
|
if client == nil {
|
||||||
|
return nil, errors.New("notion client not initialized")
|
||||||
|
}
|
||||||
|
rows := []notionapi.TableRowBlock{}
|
||||||
|
nextCursor := ""
|
||||||
|
hasMore := true
|
||||||
|
for hasMore {
|
||||||
|
blockChildren, err := client.Block.GetChildren(context.Background(), notionapi.BlockID(tableId), ¬ionapi.Pagination{
|
||||||
|
StartCursor: notionapi.Cursor(nextCursor),
|
||||||
|
PageSize: 100,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, block := range blockChildren.Results {
|
||||||
|
if block.GetType().String() == "table_row" {
|
||||||
|
tableRow, ok := block.(*notionapi.TableRowBlock)
|
||||||
|
if !ok {
|
||||||
|
slog.Error("Notion.GetAllTableRows Failed to cast block to table row")
|
||||||
|
return nil, errors.New("Notion.GetAllTableRows failed to cast block to table row")
|
||||||
|
}
|
||||||
|
rows = append(rows, *tableRow)
|
||||||
|
} else {
|
||||||
|
slog.Error(fmt.Sprintf("Block ID %s is not a table row", block.GetID()))
|
||||||
|
return nil, errors.New("Notion.GetAllTableRows block ID is not a table row")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextCursor = blockChildren.NextCursor
|
||||||
|
hasMore = blockChildren.HasMore
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteTableRow(content []string, tableId string, after string) (string, error) {
|
||||||
|
if client == nil {
|
||||||
|
return "", errors.New("notion client not initialized")
|
||||||
|
}
|
||||||
|
rich := [][]notionapi.RichText{}
|
||||||
|
for _, c := range content {
|
||||||
|
rich = append(rich, []notionapi.RichText{
|
||||||
|
{
|
||||||
|
Type: "text",
|
||||||
|
Text: ¬ionapi.Text{
|
||||||
|
Content: c,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tableRow := notionapi.TableRowBlock{
|
||||||
|
BasicBlock: notionapi.BasicBlock{
|
||||||
|
Object: "block",
|
||||||
|
Type: "table_row",
|
||||||
|
},
|
||||||
|
TableRow: notionapi.TableRow{
|
||||||
|
Cells: rich,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Block.AppendChildren(context.Background(), notionapi.BlockID(tableId), ¬ionapi.AppendBlockChildrenRequest{
|
||||||
|
After: notionapi.BlockID(after),
|
||||||
|
Children: []notionapi.Block{tableRow},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.Results[0].GetID().String(), err
|
||||||
|
}
|
||||||
297
src/util/ticktickutil/ticktickutil.go
Normal file
297
src/util/ticktickutil/ticktickutil.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package ticktickutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DateTimeLayout = "2006-01-02T15:04:05-0700"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TicktickUtil interface {
|
||||||
|
HandleAuthCode(w http.ResponseWriter, r *http.Request)
|
||||||
|
GetTasks(projectId string) []Task
|
||||||
|
HasDuplicateTask(projectId string, taskTitile string) bool
|
||||||
|
CreateTask(task Task) error
|
||||||
|
}
|
||||||
|
|
||||||
|
TicktickUtilImpl struct {
|
||||||
|
authState string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Project struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color string `json:"color,omitempty"`
|
||||||
|
SortOrder int64 `json:"sortOrder,omitempty"`
|
||||||
|
Closed bool `json:"closed,omitempty"`
|
||||||
|
GroupId string `json:"groupId,omitempty"`
|
||||||
|
ViewMode string `json:"viewMode,omitempty"`
|
||||||
|
Permission string `json:"permission,omitempty"`
|
||||||
|
Kind string `json:"kind,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Column struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProjectId string `json:"projectId"`
|
||||||
|
SortOrder int64 `json:"sortOrder,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Task struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
ProjectId string `json:"projectId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
IsAllDay bool `json:"isAllDay,omitempty"`
|
||||||
|
CompletedTime string `json:"completedTime,omitempty"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
Desc string `json:"desc,omitempty"`
|
||||||
|
DueDate string `json:"dueDate,omitempty"`
|
||||||
|
Items []interface{} `json:"items,omitempty"`
|
||||||
|
Priority int `json:"priority,omitempty"`
|
||||||
|
Reminders []string `json:"reminders,omitempty"`
|
||||||
|
RepeatFlag string `json:"repeatFlag,omitempty"`
|
||||||
|
SortOrder int64 `json:"sortOrder,omitempty"`
|
||||||
|
StartDate string `json:"startDate,omitempty"`
|
||||||
|
Status int32 `json:"status,omitempty"`
|
||||||
|
TimeZone string `json:"timeZone,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectData struct {
|
||||||
|
Project Project `json:"project"`
|
||||||
|
Tasks []Task `json:"tasks"`
|
||||||
|
Columns []Column `json:"columns,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() *TicktickUtilImpl { // TODO: Will modify Init to a proper behavior
|
||||||
|
ticktickUtilImpl := &TicktickUtilImpl{}
|
||||||
|
if !viper.InConfig("ticktick.clientId") {
|
||||||
|
slog.Error("TickTick clientId not found in config file, exiting..")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if !viper.InConfig("ticktick.clientSecret") {
|
||||||
|
slog.Error("TickTick clientSecret not found in config file, exiting..")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if viper.InConfig("ticktick.token") {
|
||||||
|
_, err := getProjects()
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "error response from TickTick: 401 Unauthorized" {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ticktickUtilImpl.beginAuth()
|
||||||
|
}
|
||||||
|
return ticktickUtilImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TicktickUtilImpl) HandleAuthCode(w http.ResponseWriter, r *http.Request) {
|
||||||
|
state := r.URL.Query().Get("state")
|
||||||
|
code := r.URL.Query().Get("code")
|
||||||
|
if state != t.authState {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Invalid state", state))
|
||||||
|
http.Error(w, "Invalid state", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
params := map[string]string{
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"scope": "tasks:read tasks:write",
|
||||||
|
"redirect_uri": viper.GetString("ticktick.redirectUri"),
|
||||||
|
}
|
||||||
|
formedParams := url.Values{}
|
||||||
|
for key, value := range params {
|
||||||
|
formedParams.Add(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", "https://ticktick.com/oauth/token", bytes.NewBufferString(formedParams.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Error creating request", err))
|
||||||
|
http.Error(w, "Error creating request", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.SetBasicAuth(viper.GetString("ticktick.clientId"), viper.GetString("ticktick.clientSecret"))
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Error sending request", err))
|
||||||
|
http.Error(w, "Error sending request", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Unexpected response status", resp.StatusCode))
|
||||||
|
http.Error(w, "Unexpected response status", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
var tokenResponse map[string]interface{}
|
||||||
|
err = decoder.Decode(&tokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Error decoding response", err))
|
||||||
|
http.Error(w, "Error decoding response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token := tokenResponse["access_token"].(string)
|
||||||
|
viper.Set("ticktick.token", token)
|
||||||
|
err = viper.WriteConfig()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("HandleAuthCode Error writing config", err))
|
||||||
|
http.Error(w, "Error writing config", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("Authorization successful"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TicktickUtilImpl) GetTasks(projectId string) []Task {
|
||||||
|
getTaskUrl := fmt.Sprintf("https://api.ticktick.com/open/v1/project/%s/data", projectId)
|
||||||
|
token := viper.GetString("ticktick.token")
|
||||||
|
req, err := http.NewRequest("GET", getTaskUrl, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.GetTasks Error creating request to TickTick", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var projectData ProjectData
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.GetTasks Error sending request to TickTick", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.GetTasks Error response from TickTick", resp.Status))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
err = decoder.Decode(&projectData)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.GetTasks Error decoding response from TickTick", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectData.Tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TicktickUtilImpl) HasDuplicateTask(projectId string, taskTitile string) bool {
|
||||||
|
tasks := t.GetTasks(projectId)
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.Title == taskTitile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TicktickUtilImpl) CreateTask(task Task) error {
|
||||||
|
if t.HasDuplicateTask(task.ProjectId, task.Title) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
token := viper.GetString("ticktick.token")
|
||||||
|
createTaskUrl := "https://api.ticktick.com/open/v1/task"
|
||||||
|
payload, err := json.Marshal(task)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.CreateTask Error marshalling", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", createTaskUrl, bytes.NewBuffer(payload))
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.CreateTask Error creating request to TickTick", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.CreateTask Error sending request to TickTick", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
slog.Warn(fmt.Sprintln("Ticktick.CreateTask Error response from TickTick", resp.Status))
|
||||||
|
return fmt.Errorf("error response from TickTick: %s", resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProjects() ([]Project, error) {
|
||||||
|
token := viper.GetString("ticktick.token")
|
||||||
|
req, err := http.NewRequest("GET", "https://api.ticktick.com/open/v1/project/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Error creating request to TickTick", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Error sending request to TickTick", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
slog.Warn(fmt.Sprintln("Error response from TickTick", resp.Status))
|
||||||
|
return nil, fmt.Errorf("error response from TickTick: %s", resp.Status)
|
||||||
|
}
|
||||||
|
var projects []Project
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
err = decoder.Decode(&projects)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintln("Error decoding response from TickTick", err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TicktickUtilImpl) beginAuth() {
|
||||||
|
if !viper.InConfig("ticktick.redirectUri") {
|
||||||
|
slog.Error("TickTick redirectUri not found in config file, exiting..")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
baseUrl := "https://ticktick.com/oauth/authorize?"
|
||||||
|
authUrl, _ := url.Parse(baseUrl)
|
||||||
|
authStateBytes := make([]byte, 6)
|
||||||
|
_, err := rand.Read(authStateBytes)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintln("Error generating auth state", err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
t.authState = hex.EncodeToString(authStateBytes)
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("client_id", viper.GetString("ticktick.clientId"))
|
||||||
|
params.Add("response_type", "code")
|
||||||
|
params.Add("redirect_uri", viper.GetString("ticktick.redirectUri"))
|
||||||
|
params.Add("state", t.authState)
|
||||||
|
params.Add("scope", "tasks:read tasks:write")
|
||||||
|
authUrl.RawQuery = params.Encode()
|
||||||
|
slog.Info(fmt.Sprintln("Please visit the following URL to authorize TickTick:", authUrl.String()))
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from app.main import app
|
|
||||||
|
|
||||||
client = TestClient(app)
|
|
||||||
|
|
||||||
|
|
||||||
def test_health() -> None:
|
|
||||||
r = client.get("/api/health")
|
|
||||||
assert r.status_code == 200
|
|
||||||
assert r.json() == {"status": "ok"}
|
|
||||||
Reference in New Issue
Block a user