├── run_frontend.sh ├── run_backend.sh ├── .env.example ├── backend └── python │ ├── start.sh │ ├── README.md │ ├── pyproject.toml │ ├── Dockerfile │ ├── main.py │ └── poetry.lock ├── .vscode └── settings.json ├── .gitignore ├── fixenv.sh ├── frontend ├── public │ ├── favicon.ico │ └── vercel.svg ├── next.config.js ├── next-env.d.ts ├── styles │ ├── globals.css │ └── Home.module.css ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ └── index.tsx ├── components │ ├── Card.tsx │ ├── dashboard │ │ ├── VitalLogo.tsx │ │ ├── HorizontalBarChart.tsx │ │ ├── InfoIcon.tsx │ │ ├── DetailedLineGraph.tsx │ │ ├── ActivityGraph.tsx │ │ ├── SleepGraph.tsx │ │ ├── customRadio.tsx │ │ ├── WorkoutPanel.tsx │ │ ├── ActivityPanel.tsx │ │ ├── SleepPanel.tsx │ │ └── LineGraph.tsx │ ├── LinkButton.tsx │ └── CreateUserVital.tsx ├── tsconfig.json ├── package.json ├── lib │ ├── client.tsx │ └── utils.tsx ├── README.md ├── Dockerfile └── models │ └── index.tsx ├── docker-compose.yml └── README.md /run_frontend.sh: -------------------------------------------------------------------------------- 1 | cd frontend/ && yarn dev -------------------------------------------------------------------------------- /run_backend.sh: -------------------------------------------------------------------------------- 1 | cd backend/python/ && ./start.sh -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITAL_API_KEY= 2 | VITAL_ENV= 3 | VITAL_REGION= -------------------------------------------------------------------------------- /backend/python/start.sh: -------------------------------------------------------------------------------- 1 | uvicorn main:app --reload --port=8000 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __pycache__ 3 | .env 4 | .DS_Store 5 | .next 6 | .env.local 7 | /**/*.env.local -------------------------------------------------------------------------------- /fixenv.sh: -------------------------------------------------------------------------------- 1 | cp .env ./backend/python/ && cat .env | sed -e s/VITAL_/NEXT_PUBLIC_VITAL_/g > ./frontend/.env.local -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryVital/quickstart/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/python/README.md: -------------------------------------------------------------------------------- 1 | # Python quickstart backend written in python 2 | 3 | ``` 4 | uvicorn main:app --reload --port=8000 5 | ``` 6 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | output: 'standalone' 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | backend-python: 4 | build: ./backend/python 5 | ports: 6 | - "8000:8000" 7 | frontend: 8 | build: "./frontend" 9 | ports: 10 | - "3000:3000" -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { ChakraProvider } from "@chakra-ui/react"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default MyApp; 14 | -------------------------------------------------------------------------------- /frontend/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { VStack } from "@chakra-ui/react"; 2 | 3 | export const Card = ({ children }: { children: any }) => { 4 | return ( 5 | 12 | {children} 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /backend/python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "quickstart" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Maitham "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | vital = "^1.1.22" 10 | fastapi = "^0.77.1" 11 | uvicorn = "^0.17.6" 12 | python-dotenv = "^0.20.0" 13 | 14 | [tool.poetry.dev-dependencies] 15 | 16 | [tool.poetry.group.dev.dependencies] 17 | cffi = "1.17.1" 18 | 19 | [build-system] 20 | requires = ["poetry-core>=1.0.0"] 21 | build-backend = "poetry.core.masonry.api" 22 | -------------------------------------------------------------------------------- /backend/python/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image. 2 | # https://hub.docker.com/_/python 3 | FROM python:3.10 4 | 5 | # Copy local code to the container image. 6 | ENV APP_HOME /app 7 | WORKDIR /app 8 | 9 | RUN pip install 'poetry==1.3.2' 10 | COPY . . 11 | COPY .env .env 12 | 13 | RUN poetry export -f requirements.txt --output requirements.txt --without-hashes 14 | RUN python -m pip install --upgrade pip 15 | 16 | RUN pip install -r requirements.txt 17 | 18 | EXPOSE 8000 19 | CMD exec uvicorn main:app --host 0.0.0.0 --port 8000 20 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vital-frontend-quickstart", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^2.2.0", 13 | "@emotion/react": "^11.9.3", 14 | "@emotion/styled": "^11.9.3", 15 | "@tryvital/vital-link": "^0.1.7", 16 | "framer-motion": "^6", 17 | "moment": "^2.29.3", 18 | "next": "12.2.0", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "react-icons": "^4.4.0", 22 | "recharts": "^2.1.12", 23 | "swr": "^1.3.0" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "17.0.32", 27 | "@types/react": "18.0.9", 28 | "@types/react-dom": "18.0.4", 29 | "eslint": "8.15.0", 30 | "eslint-config-next": "12.1.6", 31 | "typescript": "4.6.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/components/dashboard/VitalLogo.tsx: -------------------------------------------------------------------------------- 1 | import { chakra, HTMLChakraProps, useToken } from '@chakra-ui/react' 2 | 3 | export const VitalLogo = ( 4 | props: HTMLChakraProps<'svg'> & { iconColor?: string } 5 | ) => { 6 | const { iconColor = 'black', ...rest } = props 7 | const color = useToken('colors', iconColor) 8 | 9 | return ( 10 | 17 | 24 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vital quickstart 2 | 3 | To get started with Vital quickstart, start with reading our [Docs](https://docs.tryvital.io/welcome/quickstart) 4 | 5 | 6 | ### Step 1 - Obtain and set Credentials 7 | 8 | The first step is to obtain your credentials from the Vital Dashboard. 9 | 10 | Once you have done this: 11 | 12 | ``` 13 | # Copy the .env.example file: 14 | cp .env.example .env 15 | 16 | # Fill all the env variables in the .env file 17 | VITAL_API_KEY=.. 18 | VITAL_ENV=sandbox 19 | VITAL_REGION=us 20 | ``` 21 | 22 | 23 | ### Step 2a - Run using Docker 24 | 25 | ``` 26 | # Run fixenv.sh this copies the environment variables into the correct locations 27 | ./fixenv.sh 28 | 29 | # Run Docker 30 | docker compose up --build 31 | ``` 32 | 33 | ### Step 2b - Run manually 34 | 35 | 36 | ``` 37 | # Run fixenv.sh 38 | ./fixenv.sh 39 | 40 | cd backend/python 41 | 42 | # Note: must use python 3 43 | # For virtualenv users: 44 | # virtualenv venv 45 | # source venv/bin/activate 46 | poetry shell 47 | poetry install 48 | 49 | # While still in the shell, start the backend app 50 | cd ../../ && ./run_backend.sh 51 | ``` 52 | 53 | To run the frontend: 54 | 55 | ``` 56 | # Install dependencies 57 | cd quickstart/frontend 58 | npm install 59 | 60 | # Start the frontend app 61 | npm run dev 62 | 63 | # Go to http://localhost:3000 64 | ``` 65 | 66 | ### Step 3 67 | 68 | Head to localhost:3000 and begin interacting with the frontend 69 | Head to localhost:8000 to begin interacting with the backend. 70 | -------------------------------------------------------------------------------- /frontend/lib/client.tsx: -------------------------------------------------------------------------------- 1 | const API_URL = "http://0.0.0.0:8000"; 2 | 3 | const BACKEND_IS_PYTHON = true; 4 | const URL_PREFIX = BACKEND_IS_PYTHON ? API_URL : "/api"; 5 | 6 | export const fetcher = (url: string) => 7 | fetch(`${URL_PREFIX}${url}`).then((res) => res.json()); 8 | 9 | export const fetchSummaryData = ( 10 | data_type: string, 11 | userID: string, 12 | start_date: string, 13 | end_date: string, 14 | key: string 15 | ) => 16 | fetch( 17 | `${URL_PREFIX}/summary/${data_type}/${userID}?start_date=${start_date}&end_date=${end_date}` 18 | ) 19 | .then((res) => res.json()) 20 | .then((data) => data[key]); 21 | 22 | export class Client { 23 | constructor() {} 24 | 25 | getTokenFromBackend = async (userID: string) => { 26 | const data = await this._fetch("GET", `/token/${userID}`); 27 | return data; 28 | }; 29 | 30 | createUser = async (client_user_id: string) => { 31 | const data = await this._fetch("POST", `/user/`, { client_user_id }); 32 | return data; 33 | }; 34 | 35 | getUsers = async () => { 36 | const data = await this._fetch("GET", `/users/`); 37 | return data; 38 | }; 39 | 40 | _fetch = async ( 41 | method: string, 42 | resource: string, 43 | body?: Record 44 | ) => { 45 | const resp = await fetch(`${API_URL}${resource}`, { 46 | method: method, 47 | body: body ? JSON.stringify(body) : null, 48 | headers: { "content-type": "application/json" }, 49 | }); 50 | const data = await resp.json(); 51 | return data; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /frontend/components/dashboard/HorizontalBarChart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BarChart, 4 | Bar, 5 | Cell, 6 | XAxis, 7 | YAxis, 8 | Tooltip, 9 | ResponsiveContainer, 10 | } from "recharts"; 11 | 12 | const COLORS = ["#F6BDC0", "#F1959B", "#F07470", "#EA4C46", "#DC1C13"]; 13 | 14 | interface HorizontalBarChartProps { 15 | data: any; 16 | reversed?: boolean; 17 | } 18 | 19 | export const HorizontalBarChart: React.FunctionComponent< 20 | HorizontalBarChartProps 21 | > = ({ data, reversed }) => { 22 | return ( 23 | 24 | 25 | {/* */} 26 | 27 | {data.map((entry: any, index: any) => { 28 | return ; 29 | })} 30 | 31 | [`${value} min`]} 41 | labelFormatter={(x) => ""} 42 | /> 43 | 49 | 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /frontend/lib/utils.tsx: -------------------------------------------------------------------------------- 1 | import { Workout } from "../models"; 2 | import _ from "lodash"; 3 | import moment from "moment"; 4 | 5 | export const roundNum = (num:any) => Math.round((num + Number.EPSILON) * 100) / 100; 6 | 7 | export const getDiffInMinutes = (workouts: Workout[]) => { 8 | return Math.round( 9 | _.sumBy(workouts, (x) => { 10 | const diff = moment(x.time_end).diff(moment(x.time_start)); 11 | return moment.duration(diff).asMinutes(); 12 | }) 13 | ); 14 | }; 15 | 16 | export const getDiffInMins = (start:any, end:any) => { 17 | const diff = moment(end).diff(moment(start)); 18 | return Math.round(moment.duration(diff).asMinutes()); 19 | }; 20 | 21 | export const parseHrZoneData = (workouts: Workout[]) => [ 22 | { 23 | name: "Zone 1", 24 | value: Math.round(_.sumBy(workouts, (x) => x.hr_zones[0]) / 60), 25 | }, 26 | { 27 | name: "Zone 2", 28 | value: Math.round(_.sumBy(workouts, (x) => x.hr_zones[1]) / 60), 29 | }, 30 | { 31 | name: "Zone 3", 32 | value: Math.round(_.sumBy(workouts, (x) => x.hr_zones[2]) / 60), 33 | }, 34 | { 35 | name: "Zone 4", 36 | value: Math.round(_.sumBy(workouts, (x) => x.hr_zones[3]) / 60), 37 | }, 38 | { 39 | name: "Zone 5", 40 | value: Math.round(_.sumBy(workouts, (x) => x.hr_zones[4]) / 60), 41 | }, 42 | ]; 43 | 44 | export const parseSecs = (num:any) => { 45 | if (!num) return { hours: 0, mins: 0 }; 46 | const time = num / 3600; 47 | const mins = time - Math.floor(time); 48 | return { 49 | hours: Math.floor(time), 50 | minutes: Math.round(mins * 60), 51 | }; 52 | }; 53 | 54 | export const parseMins = (num:any) => { 55 | if (!num) return { hours: 0, mins: 0 }; 56 | const time = num / 60; 57 | const mins = time - Math.floor(time); 58 | return { 59 | hours: Math.floor(time) ? Math.floor(time) : 0, 60 | minutes: Math.round(mins * 60) ? Math.round(mins * 60) : 0, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /frontend/components/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/react"; 2 | import { useState, useCallback } from "react"; 3 | import { useVitalLink } from "@tryvital/vital-link"; 4 | 5 | // URL for the mock backend which creates the vital link token 6 | const API_URL = "http://0.0.0.0:8000"; 7 | 8 | const getTokenFromBackend = async (userID: string) => { 9 | const resp = await fetch(`${API_URL}/token/${userID}`); 10 | const data = await resp.json(); 11 | return data; 12 | }; 13 | 14 | export const LinkButton: React.FC<{ userID: string | null }> = ({ userID }) => { 15 | const [isLoading, setLoading] = useState(false); 16 | 17 | const onSuccess = useCallback((metadata: any) => { 18 | // Device is now connected. 19 | console.log("onSuccess", metadata); 20 | }, []); 21 | 22 | const onExit = useCallback((metadata: any) => { 23 | // User has quit the link flow. 24 | console.log("onExit", metadata); 25 | }, []); 26 | 27 | const onError = useCallback((metadata: any) => { 28 | // Error encountered in connecting device. 29 | console.log("onError", metadata); 30 | }, []); 31 | 32 | const config = { 33 | onSuccess, 34 | onExit, 35 | onError, 36 | env: process.env.NEXT_PUBLIC_VITAL_ENV as string, 37 | region: process.env.NEXT_PUBLIC_VITAL_REGION as string, 38 | }; 39 | 40 | const { open, ready, error } = useVitalLink(config); 41 | 42 | const handleVitalOpen = async () => { 43 | setLoading(true); 44 | const token = await getTokenFromBackend(userID as string); 45 | open(token.link_token); 46 | setLoading(false); 47 | }; 48 | 49 | return ( 50 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { VStack, Heading, HStack, Text, Box } from "@chakra-ui/react"; 3 | import { Card } from "../components/Card"; 4 | import { CreateUserVital } from "../components/CreateUserVital"; 5 | import { useState } from "react"; 6 | import useSWR from "swr"; 7 | import { fetcher } from "../lib/client"; 8 | import { SleepPanel } from "../components/dashboard/SleepPanel"; 9 | import { ActivityPanel } from "../components/dashboard/ActivityPanel"; 10 | 11 | const Home: NextPage = () => { 12 | const [userID, setUserID] = useState(null); 13 | const { data } = useSWR("/users/", fetcher); 14 | 15 | const usersFiltered = data?.users ? data.users : []; 16 | 17 | return ( 18 | 26 | 27 | Vital Quickstart 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 2. Visualize user data 40 | 41 | Request user data and plot activity, workout sleep and other 42 | health information. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default Home; 60 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine AS base 2 | 3 | # Install dependencies only when needed 4 | FROM base AS deps 5 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 6 | RUN apk add --no-cache libc6-compat 7 | WORKDIR /app 8 | 9 | # Install dependencies based on the preferred package manager 10 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 11 | RUN \ 12 | if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ 13 | elif [ -f package-lock.json ]; then npm ci; \ 14 | elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ 15 | else echo "Lockfile not found." && exit 1; \ 16 | fi 17 | 18 | 19 | # Rebuild the source code only when needed 20 | FROM base AS builder 21 | WORKDIR /app 22 | COPY --from=deps /app/node_modules ./node_modules 23 | COPY . . 24 | 25 | # Next.js collects completely anonymous telemetry data about general usage. 26 | # Learn more here: https://nextjs.org/telemetry 27 | # Uncomment the following line in case you want to disable telemetry during the build. 28 | # ENV NEXT_TELEMETRY_DISABLED 1 29 | 30 | RUN yarn build 31 | 32 | # If using npm comment out above and use below instead 33 | # RUN npm run build 34 | 35 | # Production image, copy all the files and run next 36 | FROM base AS runner 37 | WORKDIR /app 38 | 39 | ENV NODE_ENV production 40 | # Uncomment the following line in case you want to disable telemetry during runtime. 41 | # ENV NEXT_TELEMETRY_DISABLED 1 42 | 43 | RUN addgroup --system --gid 1001 nodejs 44 | RUN adduser --system --uid 1001 nextjs 45 | 46 | COPY --from=builder /app/public ./public 47 | 48 | # Automatically leverage output traces to reduce image size 49 | # https://nextjs.org/docs/advanced-features/output-file-tracing 50 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 51 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 52 | 53 | USER nextjs 54 | 55 | EXPOSE 3000 56 | 57 | ENV PORT 3000 58 | 59 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /frontend/components/dashboard/InfoIcon.tsx: -------------------------------------------------------------------------------- 1 | import { VStack, Box, HStack, Text, Progress } from "@chakra-ui/react"; 2 | 3 | export const InfoIcon = ({ icon, label, value }:any) => { 4 | return ( 5 | 6 | {icon} 7 | {label} 8 | 9 | {value} 10 | 11 | 12 | ); 13 | }; 14 | 15 | export const InfoText = ({ label, value1, value2, suffix1, suffix2 } :any) => { 16 | return ( 17 | 18 | 19 | {value1} 20 | 21 | {suffix1} 22 | 23 | {value2} 24 | 25 | {suffix2} 26 | 27 | 28 | 29 | 30 | {label} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export const InfoBar = ({ 37 | label, 38 | barValue, 39 | value1, 40 | value2, 41 | suffix1, 42 | suffix2, 43 | color, 44 | }:any) => { 45 | return ( 46 | 47 | 48 | 56 | {label} 57 | 58 | 59 | 60 | 67 | 68 | 69 | {value1} 70 | 71 | {suffix1} 72 | 73 | {value2} 74 | 75 | {suffix2} 76 | 77 | 78 | 79 | ); 80 | }; 81 | -------------------------------------------------------------------------------- /backend/python/main.py: -------------------------------------------------------------------------------- 1 | # TEST BACKEND IMPLEMENTATION 2 | from http.client import HTTPException 3 | from tracemalloc import start 4 | from typing import Optional 5 | from vital import Client 6 | from fastapi import FastAPI 7 | from starlette.middleware.cors import CORSMiddleware 8 | from pydantic import BaseModel 9 | import os 10 | from dotenv import load_dotenv 11 | load_dotenv() 12 | 13 | app = FastAPI() 14 | VITAL_API_KEY = os.getenv("VITAL_API_KEY") 15 | VITAL_ENVIRONMENT = os.getenv("VITAL_ENV") 16 | VITAL_REGION = os.getenv("VITAL_REGION") 17 | 18 | client = Client(api_key=VITAL_API_KEY, environment=VITAL_ENVIRONMENT, region=os.getenv("VITAL_REGION")) 19 | 20 | app.add_middleware( # type: ignore 21 | CORSMiddleware, 22 | allow_origins=["*"], 23 | allow_credentials=True, 24 | allow_methods=["*"], 25 | allow_headers=["*"], 26 | ) 27 | 28 | 29 | @app.get("/token/{user_key}") 30 | def get_token(user_key: str): 31 | return client.Link.create(user_key) 32 | 33 | 34 | class CreateUserData(BaseModel): 35 | client_user_id: str 36 | 37 | 38 | @app.post("/user/") 39 | def create_user(data: CreateUserData): 40 | return client.User.create(data.client_user_id) 41 | 42 | 43 | @app.get("/users/") 44 | def get_users(): 45 | return client.User.get_all() 46 | 47 | 48 | @app.get("/summary/{data_type}/{user_id}") 49 | def get_users(data_type: str, user_id: str, start_date: str, end_date: str): 50 | func_map = { 51 | "sleep": client.Sleep.get, 52 | "activity": client.Activity.get, 53 | "body": client.Body.get, 54 | "workouts": client.Workouts.get, 55 | } 56 | func = func_map.get(data_type) 57 | if not func: 58 | raise HTTPException(400, "Failed to find data type") 59 | data = func(user_id, start_date, end_date) 60 | return data 61 | 62 | 63 | @app.get("/summary/{user_id}") 64 | def get_users(user_id: str, start_date: str, end_date: str): 65 | sleep = client.Sleep.get(user_id, start_date, end_date) 66 | activity = client.Activity.get(user_id, start_date, end_date) 67 | body = client.Body.get(user_id, start_date, end_date) 68 | workouts = client.Workouts.get(user_id, start_date, end_date) 69 | return {"sleep": sleep, "activity": activity, "body": body, "workouts": workouts} 70 | -------------------------------------------------------------------------------- /frontend/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /frontend/components/dashboard/DetailedLineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { 4 | LineChart, 5 | Line, 6 | XAxis, 7 | YAxis, 8 | CartesianGrid, 9 | Tooltip, 10 | Legend, 11 | AreaChart, 12 | Area, 13 | ResponsiveContainer, 14 | } from "recharts"; 15 | import { HStack, Text } from "@chakra-ui/react"; 16 | import { IoBed } from "react-icons/io5"; 17 | import { AiFillHeart } from "react-icons/ai"; 18 | import { FaRunning } from "react-icons/fa"; 19 | import _ from "lodash"; 20 | 21 | interface DataPoint { 22 | value: number; 23 | } 24 | 25 | interface LineProps { 26 | data: DataPoint[]; 27 | } 28 | 29 | export const DetailedLineGraph: React.FunctionComponent = ({ 30 | data, 31 | }) => { 32 | const maxValue = _.maxBy(data, (x) => x.value); 33 | const max = Math.ceil(maxValue?.value || 0 / 25) * 25; 34 | 35 | const shouldRotate = data.length > 2000; 36 | return ( 37 | 38 | 39 | moment.unix(unixTime).format("HH:mm A")} 54 | /> 55 | el < max + 50)} 60 | domain={[0, `${max}`]} 61 | /> 62 | 63 | 64 | 72 | moment.unix(x).format("HH:mm A")} 74 | labelStyle={{ fontSize: 12, color: "gray" }} 75 | contentStyle={{ borderRadius: 10 }} 76 | formatter={(value: any, name: any, props: any) => { 77 | if (name === "value") { 78 | return [ 79 | 80 | 81 | {value} 82 | , 83 | ]; 84 | } 85 | }} 86 | /> 87 | 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /frontend/components/dashboard/ActivityGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | AreaChart, 9 | Area, 10 | ResponsiveContainer, 11 | } from "recharts"; 12 | import _ from "lodash"; 13 | import { Activity } from "../../models"; 14 | import { parseMins, parseSecs } from "../../lib/utils"; 15 | 16 | export const ActivityGraph: React.FunctionComponent<{ data: Activity[] }> = ({ 17 | data, 18 | }) => { 19 | const fixedData = data.map((el) => ({ ...el, date: moment(el.date).unix() })); 20 | return ( 21 | 22 | 23 | 34 | moment.unix(unixTime).format("ddd MMM D") 35 | } 36 | /> 37 | el < max + 50)} 42 | // domain={[0, `${max}`]} 43 | tickFormatter={(value) => { 44 | const time = parseMins(value); 45 | return `${time.hours ? time.hours : 0}h ${ 46 | time.minutes ? time.minutes : 0 47 | } min`; 48 | }} 49 | /> 50 | 51 | moment.unix(x).format("ddd MMM D")} 53 | labelStyle={{ fontSize: 12, color: "gray" }} 54 | itemStyle={{ color: "rgba(153, 192, 74,1)" }} 55 | contentStyle={{ borderRadius: 10 }} 56 | formatter={(value:any, name:any, props:any) => { 57 | const time = parseMins(value); 58 | return `${time.hours ? time.hours : 0}h ${ 59 | time.minutes ? time.minutes : 0 60 | } min`; 61 | }} 62 | /> 63 | 64 | 72 | 79 | 86 | 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /frontend/components/dashboard/SleepGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { 4 | LineChart, 5 | Line, 6 | XAxis, 7 | YAxis, 8 | CartesianGrid, 9 | Tooltip, 10 | Legend, 11 | AreaChart, 12 | Area, 13 | ResponsiveContainer, 14 | } from "recharts"; 15 | import { HStack, Text } from "@chakra-ui/react"; 16 | import { IoBed } from "react-icons/io5"; 17 | import { AiFillHeart } from "react-icons/ai"; 18 | import { FaRunning } from "react-icons/fa"; 19 | import _ from "lodash"; 20 | import { Sleep } from "../../models"; 21 | import { parseSecs } from "../../lib/utils"; 22 | 23 | export const SleepGraph: React.FunctionComponent<{ data: Sleep[] }> = ({ 24 | data, 25 | }) => { 26 | const fixedData = data.map((el) => ({ ...el, date: moment(el.date).unix() })); 27 | return ( 28 | 29 | 30 | 41 | moment.unix(unixTime).format("ddd MMM D") 42 | } 43 | /> 44 | el < max + 50)} 49 | // domain={[0, `${max}`]} 50 | tickFormatter={(value) => { 51 | const time = parseSecs(value); 52 | return `${time.hours}h ${time.minutes ? time.minutes : 0} min`; 53 | }} 54 | /> 55 | 56 | moment.unix(x).format("ddd MMM D")} 58 | labelStyle={{ fontSize: 12, color: "gray" }} 59 | contentStyle={{ borderRadius: 10 }} 60 | formatter={(value:any, name:any, props:any) => { 61 | const time = parseSecs(value); 62 | return `${time.hours}h ${time.minutes}min`; 63 | }} 64 | /> 65 | 66 | 74 | 81 | 88 | 95 | 96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /frontend/components/dashboard/customRadio.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Box, HStack, useRadio, useRadioGroup } from "@chakra-ui/react"; 3 | 4 | export const RadioCard = (props) => { 5 | const { getInputProps, getCheckboxProps } = useRadio(props); 6 | 7 | const input = getInputProps(); 8 | const checkbox = getCheckboxProps(); 9 | 10 | return ( 11 | 12 | 13 | 31 | {props.children} 32 | 33 | 34 | ); 35 | }; 36 | 37 | export const RadioCardSquare = (props) => { 38 | const { getInputProps, getCheckboxProps } = useRadio(props); 39 | 40 | const input = getInputProps(); 41 | const checkbox = getCheckboxProps(); 42 | 43 | return ( 44 | 45 | 46 | 65 | {props.children} 66 | 67 | 68 | ); 69 | }; 70 | 71 | interface RadioButtonProps { 72 | options: Any; 73 | onChange: Any; 74 | selectedColor?: string; 75 | isSquare?: boolean; 76 | defaultValue: string; 77 | } 78 | 79 | // Step 2: Use the `useRadioGroup` hook to control a group of custom radios. 80 | export const RadioButtons: React.FunctionComponent = ({ 81 | options, 82 | onChange, 83 | selectedColor, 84 | isSquare, 85 | defaultValue, 86 | }) => { 87 | const { getRootProps, getRadioProps } = useRadioGroup({ 88 | name: "framework", 89 | defaultValue: defaultValue, 90 | onChange: onChange, 91 | }); 92 | 93 | const group = getRootProps(); 94 | 95 | return ( 96 | 102 | {options.map((value) => { 103 | const radio = getRadioProps({ value }); 104 | return isSquare ? ( 105 | 106 | {value} 107 | 108 | ) : ( 109 | 110 | {value} 111 | 112 | ); 113 | })} 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /frontend/models/index.tsx: -------------------------------------------------------------------------------- 1 | export interface User { 2 | uid: string; 3 | } 4 | 5 | export interface ConnectedSource { 6 | source_id: Number; 7 | id: string; 8 | } 9 | 10 | export interface Profile { 11 | auth0_id: string; 12 | avatar: string; 13 | email: string; 14 | id: string; 15 | name: string; 16 | } 17 | 18 | export interface Invite { 19 | id: string; 20 | email: string; 21 | role?: string; 22 | invited_at: string; 23 | team_id: string; 24 | verification_code: string; 25 | invited_by: string; 26 | team_name: string; 27 | } 28 | 29 | export interface Team { 30 | id: string; 31 | name: string; 32 | description?: string; 33 | members: Member[]; 34 | sandbox_client_id: string; 35 | sandbox_client_secret: string; 36 | prod_client_id: string; 37 | prod_client_secret: string; 38 | sandbox_connected_sources: ConnectedSource[]; 39 | prod_connected_sources: ConnectedSource[]; 40 | prod_users: User[]; 41 | sandbox_users: User[]; 42 | sandbox_airtable_api_key: string; 43 | sandbox_airtable_base_id: string; 44 | prod_airtable_api_key: string; 45 | prod_airtable_base_id: string; 46 | sandbox_webhook_secret: string; 47 | prod_webhook_secret: string; 48 | has_production_access: boolean; 49 | has_dashboard_access: boolean; 50 | prod_application_status?: "applied" | "success"; 51 | invites: Invite[]; 52 | } 53 | 54 | export interface Member { 55 | auth0_id: string; 56 | avatar: string; 57 | email: string; 58 | id: string; 59 | name: string; 60 | teams: Team[]; 61 | role: string; 62 | } 63 | 64 | export interface CreateUserResponse { 65 | client_user_id: string; 66 | team_id: string; 67 | } 68 | 69 | export interface LinkTokenResponse { 70 | user_key: string; 71 | team_id: string; 72 | } 73 | 74 | export interface HeartRate { 75 | value: number; 76 | timestamp: number; 77 | } 78 | export interface Source { 79 | name: string; 80 | slug: string; 81 | logo: string; 82 | auth_type: string; 83 | } 84 | 85 | export interface Sport { 86 | name: string; 87 | icon: string; 88 | category: string; 89 | id: string; 90 | } 91 | export interface Workout { 92 | average_hr: number; 93 | calories: number; 94 | distance: number; 95 | heart_rate: HeartRate[]; 96 | hr_zones: number[]; 97 | id: string; 98 | max_hr: number; 99 | priority: number; 100 | source: Source; 101 | sport: Sport; 102 | time_end: string; 103 | time_start: string; 104 | } 105 | 106 | export interface Sleep { 107 | average_hrv: number; 108 | awake: number; 109 | bedtime_start: string; 110 | bedtime_stop: string; 111 | date: string; 112 | deep: number; 113 | duration: number; 114 | efficiency: number; 115 | hr_average: number; 116 | hr_lowest: number; 117 | latency: number; 118 | light: number; 119 | priority: number; 120 | rem: number; 121 | respiratory_rate: number; 122 | source: Source; 123 | temperature_delta: number; 124 | total: number; 125 | } 126 | 127 | export interface Activity { 128 | calories_active: number; 129 | calories_total: number; 130 | daily_movement: number; 131 | date: string; 132 | high: number; 133 | id: string; 134 | low: number; 135 | medium: number; 136 | priority: number; 137 | source: Source; 138 | steps: null; 139 | user_id: string; 140 | } 141 | 142 | export interface Webhook { 143 | id: string; 144 | url: string; 145 | event_type: string; 146 | last_event_sent: string; 147 | } 148 | -------------------------------------------------------------------------------- /frontend/components/dashboard/WorkoutPanel.tsx: -------------------------------------------------------------------------------- 1 | import { VStack, Box, HStack, Avatar, Text, Heading } from "@chakra-ui/react"; 2 | import { DetailedLineGraph } from "./DetailedLineGraph"; 3 | import moment from "moment"; 4 | import { RiHeartPulseFill } from "react-icons/ri"; 5 | import { AiFillHeart, AiFillFire } from "react-icons/ai"; 6 | import { IoIosTimer } from "react-icons/io"; 7 | import { getDiffInMins, parseHrZoneData } from "../../lib/utils"; 8 | import _ from "lodash"; 9 | import { HorizontalBarChart } from "./HorizontalBarChart"; 10 | import { InfoIcon } from "./InfoIcon"; 11 | import { Workout } from "../../models"; 12 | 13 | const WorkoutCard: React.FunctionComponent<{ workout: Workout }> = ({ 14 | workout, 15 | }) => { 16 | const hrZoneData = parseHrZoneData([workout]); 17 | 18 | return ( 19 | 29 | 30 | 31 | 32 | {workout.sport.name} 33 | 34 | {moment(workout.time_start).format("ddd Do HH:MM A")} -{" "} 35 | {moment(workout.time_end).format("ddd Do HH:MM A")} 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | } 49 | value={workout.max_hr} 50 | /> 51 | } 54 | value={workout.average_hr} 55 | /> 56 | } 59 | value={`${getDiffInMins(workout.time_start, workout.time_end)}`} 60 | /> 61 | } 64 | value={workout.calories} 65 | /> 66 | 67 | 68 | 69 | ({ 71 | ...el, 72 | timestamp: moment(el.timestamp).unix(), 73 | }))} 74 | /> 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export const WorkoutPanel = ({ workouts }:any) => { 82 | ({ workouts }); 83 | const workoutsSorted = _.orderBy( 84 | workouts, 85 | (x) => moment(x.time_start).unix(), 86 | "desc" 87 | ); 88 | return ( 89 | <> 90 | {workoutsSorted.map((el) => ( 91 | 92 | ))} 93 | 94 | {workoutsSorted.length === 0 && ( 95 | 105 | 106 | 107 | This user doesn't have any workouts. 108 | 109 | 110 | 111 | )} 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /frontend/components/CreateUserVital.tsx: -------------------------------------------------------------------------------- 1 | import { Heading, HStack, VStack } from "@chakra-ui/react"; 2 | import { Card } from "./Card"; 3 | import { 4 | Text, 5 | Button, 6 | InputGroup, 7 | Input, 8 | InputRightElement, 9 | Image, 10 | } from "@chakra-ui/react"; 11 | import { useState } from "react"; 12 | import { Client } from "../lib/client"; 13 | import { LinkButton } from "./LinkButton"; 14 | 15 | const CreateUser = ({ onCreate }: { onCreate: any }) => { 16 | const [clientUserId, setClientUserId] = useState(""); 17 | const [isLoading, setLoading] = useState(false); 18 | 19 | const handleClick = async () => { 20 | // fetch data 21 | const client = new Client(); 22 | if (clientUserId) { 23 | setLoading(true); 24 | try { 25 | const data = await client.createUser(clientUserId); 26 | onCreate(data.user_id); 27 | setClientUserId(""); 28 | setLoading(false); 29 | } catch (e) { 30 | console.log("Failed to create"); 31 | setLoading(false); 32 | } 33 | } 34 | }; 35 | 36 | return ( 37 | 38 | setClientUserId(e.target.value)} 43 | /> 44 | 45 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export const CreateUserVital = ({ 59 | users, 60 | onCreate, 61 | onSelect, 62 | }: { 63 | users: any; 64 | onCreate: any; 65 | onSelect: any; 66 | }) => { 67 | return ( 68 | 69 | 1. Create a Vital user 70 | 71 | First step is to create a vital user using a unique id you have stored 72 | against your user. 73 | 74 | 75 | 76 | 77 | Existing Users (Most recent 5) 78 | 79 | 80 | {users 81 | .sort((a: any, b: any) => { 82 | return a.created_on < b.created_on 83 | ? -1 84 | : a.created_on > b.created_on 85 | ? 1 86 | : 0; 87 | }) 88 | .reverse() 89 | .slice(0, 5) 90 | .map((el: any) => { 91 | return ( 92 | 93 | 98 | 99 | vital_user_id: {el.user_id} 100 | 101 | {el.client_user_id} 102 | 103 | 104 | {el.connected_sources.map((el: any) => ( 105 | 112 | ))} 113 | 114 | 115 | 116 | 119 | 120 | 121 | 122 | 123 | ); 124 | })} 125 | 126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /frontend/components/dashboard/ActivityPanel.tsx: -------------------------------------------------------------------------------- 1 | import { VStack, Box, HStack, Heading } from "@chakra-ui/react"; 2 | import { ActivityGraph } from "./ActivityGraph"; 3 | import moment from "moment"; 4 | import { parseMins } from "../../lib/utils"; 5 | import { fetchSummaryData } from "../../lib/client"; 6 | import _ from "lodash"; 7 | import { Activity, Sleep } from "../../models"; 8 | import { InfoBar, InfoText } from "./InfoIcon"; 9 | import { useState } from "react"; 10 | import useSWR from "swr"; 11 | import { RadioButtons } from "./customRadio"; 12 | 13 | const defaultActvity = { 14 | calories_active: 0, 15 | calories_total: 0, 16 | daily_movement: 0, 17 | date: new Date().toISOString(), 18 | high: 0, 19 | id: "123131", 20 | low: 0, 21 | medium: 0, 22 | priority: 0, 23 | source: { 24 | name: "Not loaded", 25 | logo: null, 26 | }, 27 | steps: 0, 28 | user_id: "", 29 | }; 30 | 31 | const ActivityCard: React.FunctionComponent<{ latestActivity: Activity }> = ({ 32 | latestActivity, 33 | }) => { 34 | const latestActivitydefault = latestActivity 35 | ? latestActivity 36 | : defaultActvity; 37 | 38 | const high = parseMins(latestActivitydefault?.high); 39 | const medium = parseMins(latestActivitydefault?.medium); 40 | const low = parseMins(latestActivitydefault?.low); 41 | 42 | return ( 43 | 44 | 45 | {moment(latestActivitydefault?.date).format("Do ddd MMM")} 46 | 47 | 48 | 49 | 56 | 63 | 64 | 65 | 74 | 83 | 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | export const ActivityPanel = ({ userId }: { userId: any }) => { 99 | const [startDate, setStartDate] = useState( 100 | moment().subtract(7, "days").toISOString() 101 | ); 102 | const [endDate, setEndDate] = useState(moment().toISOString()); 103 | 104 | const { data: activity = [], error: errorSleep } = useSWR( 105 | userId ? ["activity", userId, startDate, endDate, "activity"] : null, 106 | fetchSummaryData 107 | ); 108 | 109 | const handleDateChange = (period: "1w" | "1m") => { 110 | switch (period) { 111 | case "1m": 112 | setStartDate(moment().subtract(30, "days").toISOString()); 113 | return; 114 | case "1w": 115 | setStartDate(moment().subtract(7, "days").toISOString()); 116 | return; 117 | default: 118 | return; 119 | } 120 | }; 121 | const activitySorted = _.orderBy( 122 | activity, 123 | (x) => moment(x.date).unix(), 124 | "desc" 125 | ); 126 | return ( 127 | 137 | {activitySorted && } 138 | 139 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ); 153 | }; 154 | -------------------------------------------------------------------------------- /frontend/components/dashboard/SleepPanel.tsx: -------------------------------------------------------------------------------- 1 | import { VStack, Box, HStack, Heading } from "@chakra-ui/react"; 2 | import { SleepGraph } from "./SleepGraph"; 3 | import moment from "moment"; 4 | import { parseSecs } from "../../lib/utils"; 5 | import { fetchSummaryData } from "../../lib/client"; 6 | 7 | import _ from "lodash"; 8 | import { Sleep } from "../../models"; 9 | import { InfoBar, InfoText } from "./InfoIcon"; 10 | import { useState } from "react"; 11 | import useSWR from "swr"; 12 | import { RadioButtons } from "./customRadio"; 13 | 14 | const SleepDayCard: React.FunctionComponent<{ latestSleep: Sleep }> = ({ 15 | latestSleep, 16 | }) => { 17 | const totalTime = parseSecs(latestSleep?.total); 18 | const rem = parseSecs(latestSleep?.rem); 19 | const deep = parseSecs(latestSleep?.deep); 20 | const latency = parseSecs(latestSleep?.latency); 21 | 22 | return ( 23 | 24 | 25 | {moment(latestSleep?.bedtime_start).format("Do ddd HH:mm A")} -{" "} 26 | {moment(latestSleep?.bedtime_stop).format("HH:mm A")} 27 | 28 | 29 | 30 | 37 | 44 | 51 | 52 | 53 | 62 | 71 | 80 | 89 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | 104 | export const SleepPanel = ({ userId }:any) => { 105 | const [startDate, setStartDate] = useState( 106 | moment().subtract(7, "days").toISOString() 107 | ); 108 | const [endDate, setEndDate] = useState(moment().toISOString()); 109 | 110 | const { data: sleeps = [], error: errorSleep } = useSWR( 111 | userId ? ["sleep", userId, startDate, endDate, "sleep"] : null, 112 | fetchSummaryData 113 | ); 114 | 115 | const handleDateChange = (period: "1w" | "1m") => { 116 | switch (period) { 117 | case "1m": 118 | setStartDate(moment().subtract(30, "days").toISOString()); 119 | return; 120 | case "1w": 121 | setStartDate(moment().subtract(7, "days").toISOString()); 122 | return; 123 | default: 124 | return; 125 | } 126 | }; 127 | const sleepsSorted = _.orderBy(sleeps, (x) => moment(x.date).unix(), "desc"); 128 | return ( 129 | 139 | {sleeps && } 140 | 141 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | ); 154 | }; 155 | -------------------------------------------------------------------------------- /frontend/components/dashboard/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import moment from "moment"; 3 | import { 4 | LineChart, 5 | Line, 6 | XAxis, 7 | YAxis, 8 | Tooltip, 9 | Legend, 10 | ResponsiveContainer, 11 | } from "recharts"; 12 | import { HStack, Text } from "@chakra-ui/react"; 13 | import { IoBed } from "react-icons/io5"; 14 | import { AiFillHeart } from "react-icons/ai"; 15 | import { FaRunning } from "react-icons/fa"; 16 | 17 | interface LineGraphProps { 18 | data: any; 19 | expanded?: boolean; 20 | } 21 | 22 | export const LineGraph: React.FunctionComponent = ({ 23 | data, 24 | expanded, 25 | }) => { 26 | return ( 27 | 28 | 29 | moment.unix(unixTime).format("DD")} 38 | /> 39 | 48 | 58 | 68 | moment.unix(x).format("ddd Do YYYY")} 70 | formatter={(value: any, name: any, props: any) => { 71 | if (name === "sleep_total") { 72 | const v = Math.round((value / 3600) * 100) / 100; 73 | var decimal = v - Math.floor(v); 74 | var mins = Math.round(decimal * 60); 75 | return [ 76 | 77 | 78 | 79 | {Math.floor(v)}hr {mins}min 80 | 81 | , 82 | ]; 83 | } 84 | if (name === "average_hr") { 85 | return [ 86 | 87 | 88 | {value} 89 | , 90 | ]; 91 | } 92 | if (name === "calories_active") { 93 | return [ 94 | 95 | 96 | {value} 97 | , 98 | ]; 99 | } 100 | return value; 101 | }} 102 | itemStyle={{ padding: 0 }} 103 | labelStyle={{ fontSize: 12, color: "gray" }} 104 | contentStyle={{ borderRadius: 10 }} 105 | /> 106 | 115 | 124 | 133 | { 136 | if (value === "sleep_total") { 137 | const v = Math.round((value / 3600) * 100) / 100; 138 | var decimal = v - Math.floor(v); 139 | var mins = Math.round(decimal * 60); 140 | return [ 141 | 142 | 143 | Total Sleep 144 | , 145 | ]; 146 | } 147 | if (value === "average_hr") { 148 | return [ 149 | 150 | 151 | Average RHR 152 | , 153 | ]; 154 | } 155 | if (value === "calories_active") { 156 | return [ 157 | 158 | 159 | Active Calories 160 | , 161 | ]; 162 | } 163 | return value; 164 | }} 165 | /> 166 | 167 | 168 | ); 169 | }; 170 | -------------------------------------------------------------------------------- /backend/python/poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "anyio" 5 | version = "3.5.0" 6 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 7 | optional = false 8 | python-versions = ">=3.6.2" 9 | files = [ 10 | {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, 11 | {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, 12 | ] 13 | 14 | [package.dependencies] 15 | idna = ">=2.8" 16 | sniffio = ">=1.1" 17 | 18 | [package.extras] 19 | doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 20 | test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] 21 | trio = ["trio (>=0.16)"] 22 | 23 | [[package]] 24 | name = "arrow" 25 | version = "1.2.2" 26 | description = "Better dates & times for Python" 27 | optional = false 28 | python-versions = ">=3.6" 29 | files = [ 30 | {file = "arrow-1.2.2-py3-none-any.whl", hash = "sha256:d622c46ca681b5b3e3574fcb60a04e5cc81b9625112d5fb2b44220c36c892177"}, 31 | {file = "arrow-1.2.2.tar.gz", hash = "sha256:05caf1fd3d9a11a1135b2b6f09887421153b94558e5ef4d090b567b47173ac2b"}, 32 | ] 33 | 34 | [package.dependencies] 35 | python-dateutil = ">=2.7.0" 36 | 37 | [[package]] 38 | name = "asgiref" 39 | version = "3.5.1" 40 | description = "ASGI specs, helper code, and adapters" 41 | optional = false 42 | python-versions = ">=3.7" 43 | files = [ 44 | {file = "asgiref-3.5.1-py3-none-any.whl", hash = "sha256:45a429524fba18aba9d512498b19d220c4d628e75b40cf5c627524dbaebc5cc1"}, 45 | {file = "asgiref-3.5.1.tar.gz", hash = "sha256:fddeea3c53fa99d0cdb613c3941cc6e52d822491fc2753fba25768fb5bf4e865"}, 46 | ] 47 | 48 | [package.extras] 49 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 50 | 51 | [[package]] 52 | name = "auth0-python" 53 | version = "3.23.0" 54 | description = "Auth0 Python SDK" 55 | optional = false 56 | python-versions = ">=2.7, !=3.0.*, !=3.1.*" 57 | files = [ 58 | {file = "auth0-python-3.23.0.tar.gz", hash = "sha256:dc7e5ccbc1ee36138656324ec9a1a530c6890177e242d95b25d3e6919dce362b"}, 59 | {file = "auth0_python-3.23.0-py2.py3-none-any.whl", hash = "sha256:8a1950fc488afae15982751bd4bf27f22da826795b571297c0c92f48ca9f0a31"}, 60 | ] 61 | 62 | [package.dependencies] 63 | pyjwt = {version = ">=1.7.1", extras = ["crypto"]} 64 | requests = ">=2.14.0" 65 | 66 | [package.extras] 67 | test = ["coverage", "mock (>=1.3.0)", "pre-commit"] 68 | 69 | [[package]] 70 | name = "certifi" 71 | version = "2021.10.8" 72 | description = "Python package for providing Mozilla's CA Bundle." 73 | optional = false 74 | python-versions = "*" 75 | files = [ 76 | {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, 77 | {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, 78 | ] 79 | 80 | [[package]] 81 | name = "cffi" 82 | version = "1.17.1" 83 | description = "Foreign Function Interface for Python calling C code." 84 | optional = false 85 | python-versions = ">=3.8" 86 | files = [ 87 | {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, 88 | {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, 89 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, 90 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, 91 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, 92 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, 93 | {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, 94 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, 95 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, 96 | {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, 97 | {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, 98 | {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, 99 | {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, 100 | {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, 101 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, 102 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, 103 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, 104 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, 105 | {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, 106 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, 107 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, 108 | {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, 109 | {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, 110 | {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, 111 | {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, 112 | {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, 113 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, 114 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, 115 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, 116 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, 117 | {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, 118 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, 119 | {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, 120 | {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, 121 | {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, 122 | {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, 123 | {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, 124 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, 125 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, 126 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, 127 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, 128 | {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, 129 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, 130 | {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, 131 | {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, 132 | {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, 133 | {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, 134 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, 135 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, 136 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, 137 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, 138 | {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, 139 | {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, 140 | {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, 141 | {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, 142 | {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, 143 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, 144 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, 145 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, 146 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, 147 | {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, 148 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, 149 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, 150 | {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, 151 | {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, 152 | {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, 153 | {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, 154 | ] 155 | 156 | [package.dependencies] 157 | pycparser = "*" 158 | 159 | [[package]] 160 | name = "charset-normalizer" 161 | version = "2.0.12" 162 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 163 | optional = false 164 | python-versions = ">=3.5.0" 165 | files = [ 166 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 167 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 168 | ] 169 | 170 | [package.extras] 171 | unicode-backport = ["unicodedata2"] 172 | 173 | [[package]] 174 | name = "click" 175 | version = "8.1.3" 176 | description = "Composable command line interface toolkit" 177 | optional = false 178 | python-versions = ">=3.7" 179 | files = [ 180 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 181 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 182 | ] 183 | 184 | [package.dependencies] 185 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 186 | 187 | [[package]] 188 | name = "colorama" 189 | version = "0.4.4" 190 | description = "Cross-platform colored terminal text." 191 | optional = false 192 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 193 | files = [ 194 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 195 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 196 | ] 197 | 198 | [[package]] 199 | name = "cryptography" 200 | version = "37.0.2" 201 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 202 | optional = false 203 | python-versions = ">=3.6" 204 | files = [ 205 | {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, 206 | {file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, 207 | {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, 208 | {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, 209 | {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, 210 | {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, 211 | {file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, 212 | {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, 213 | {file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, 214 | {file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, 215 | {file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, 216 | {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, 217 | {file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, 218 | {file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, 219 | {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, 220 | {file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, 221 | {file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, 222 | {file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, 223 | {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, 224 | {file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, 225 | {file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, 226 | {file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, 227 | ] 228 | 229 | [package.dependencies] 230 | cffi = ">=1.12" 231 | 232 | [package.extras] 233 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 234 | docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] 235 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 236 | sdist = ["setuptools-rust (>=0.11.4)"] 237 | ssh = ["bcrypt (>=3.1.5)"] 238 | test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] 239 | 240 | [[package]] 241 | name = "fastapi" 242 | version = "0.77.1" 243 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 244 | optional = false 245 | python-versions = ">=3.6.1" 246 | files = [ 247 | {file = "fastapi-0.77.1-py3-none-any.whl", hash = "sha256:041d8935a13c8f22a7ef80c76d8eea54953c44476d7057e7734138ee8951b9dc"}, 248 | {file = "fastapi-0.77.1.tar.gz", hash = "sha256:6dfa715a334d1fafedcea33d74c7382afd2ce123093f83be2f4e322e89d45e22"}, 249 | ] 250 | 251 | [package.dependencies] 252 | pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" 253 | starlette = "0.19.1" 254 | 255 | [package.extras] 256 | all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] 257 | dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] 258 | doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] 259 | test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] 260 | 261 | [[package]] 262 | name = "h11" 263 | version = "0.13.0" 264 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 265 | optional = false 266 | python-versions = ">=3.6" 267 | files = [ 268 | {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, 269 | {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, 270 | ] 271 | 272 | [[package]] 273 | name = "idna" 274 | version = "3.3" 275 | description = "Internationalized Domain Names in Applications (IDNA)" 276 | optional = false 277 | python-versions = ">=3.5" 278 | files = [ 279 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 280 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 281 | ] 282 | 283 | [[package]] 284 | name = "importlib-metadata" 285 | version = "3.10.1" 286 | description = "Read metadata from Python packages" 287 | optional = false 288 | python-versions = ">=3.6" 289 | files = [ 290 | {file = "importlib_metadata-3.10.1-py3-none-any.whl", hash = "sha256:2ec0faae539743ae6aaa84b49a169670a465f7f5d64e6add98388cc29fd1f2f6"}, 291 | {file = "importlib_metadata-3.10.1.tar.gz", hash = "sha256:c9356b657de65c53744046fa8f7358afe0714a1af7d570c00c3835c2d724a7c1"}, 292 | ] 293 | 294 | [package.dependencies] 295 | zipp = ">=0.5" 296 | 297 | [package.extras] 298 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 299 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 300 | 301 | [[package]] 302 | name = "pycparser" 303 | version = "2.21" 304 | description = "C parser in Python" 305 | optional = false 306 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 307 | files = [ 308 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 309 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 310 | ] 311 | 312 | [[package]] 313 | name = "pydantic" 314 | version = "1.10.21" 315 | description = "Data validation and settings management using python type hints" 316 | optional = false 317 | python-versions = ">=3.7" 318 | files = [ 319 | {file = "pydantic-1.10.21-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:245e486e0fec53ec2366df9cf1cba36e0bbf066af7cd9c974bbbd9ba10e1e586"}, 320 | {file = "pydantic-1.10.21-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c54f8d4c151c1de784c5b93dfbb872067e3414619e10e21e695f7bb84d1d1fd"}, 321 | {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b64708009cfabd9c2211295144ff455ec7ceb4c4fb45a07a804309598f36187"}, 322 | {file = "pydantic-1.10.21-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a148410fa0e971ba333358d11a6dea7b48e063de127c2b09ece9d1c1137dde4"}, 323 | {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:36ceadef055af06e7756eb4b871cdc9e5a27bdc06a45c820cd94b443de019bbf"}, 324 | {file = "pydantic-1.10.21-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0501e1d12df6ab1211b8cad52d2f7b2cd81f8e8e776d39aa5e71e2998d0379f"}, 325 | {file = "pydantic-1.10.21-cp310-cp310-win_amd64.whl", hash = "sha256:c261127c275d7bce50b26b26c7d8427dcb5c4803e840e913f8d9df3f99dca55f"}, 326 | {file = "pydantic-1.10.21-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b6350b68566bb6b164fb06a3772e878887f3c857c46c0c534788081cb48adf4"}, 327 | {file = "pydantic-1.10.21-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:935b19fdcde236f4fbf691959fa5c3e2b6951fff132964e869e57c70f2ad1ba3"}, 328 | {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6a04efdcd25486b27f24c1648d5adc1633ad8b4506d0e96e5367f075ed2e0b"}, 329 | {file = "pydantic-1.10.21-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1ba253eb5af8d89864073e6ce8e6c8dec5f49920cff61f38f5c3383e38b1c9f"}, 330 | {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:57f0101e6c97b411f287a0b7cf5ebc4e5d3b18254bf926f45a11615d29475793"}, 331 | {file = "pydantic-1.10.21-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e85834f0370d737c77a386ce505c21b06bfe7086c1c568b70e15a568d9670d"}, 332 | {file = "pydantic-1.10.21-cp311-cp311-win_amd64.whl", hash = "sha256:6a497bc66b3374b7d105763d1d3de76d949287bf28969bff4656206ab8a53aa9"}, 333 | {file = "pydantic-1.10.21-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ed4a5f13cf160d64aa331ab9017af81f3481cd9fd0e49f1d707b57fe1b9f3ae"}, 334 | {file = "pydantic-1.10.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b7693bb6ed3fbe250e222f9415abb73111bb09b73ab90d2d4d53f6390e0ccc1"}, 335 | {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185d5f1dff1fead51766da9b2de4f3dc3b8fca39e59383c273f34a6ae254e3e2"}, 336 | {file = "pydantic-1.10.21-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38e6d35cf7cd1727822c79e324fa0677e1a08c88a34f56695101f5ad4d5e20e5"}, 337 | {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1d7c332685eafacb64a1a7645b409a166eb7537f23142d26895746f628a3149b"}, 338 | {file = "pydantic-1.10.21-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c9b782db6f993a36092480eeaab8ba0609f786041b01f39c7c52252bda6d85f"}, 339 | {file = "pydantic-1.10.21-cp312-cp312-win_amd64.whl", hash = "sha256:7ce64d23d4e71d9698492479505674c5c5b92cda02b07c91dfc13633b2eef805"}, 340 | {file = "pydantic-1.10.21-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0067935d35044950be781933ab91b9a708eaff124bf860fa2f70aeb1c4be7212"}, 341 | {file = "pydantic-1.10.21-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5e8148c2ce4894ce7e5a4925d9d3fdce429fb0e821b5a8783573f3611933a251"}, 342 | {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4973232c98b9b44c78b1233693e5e1938add5af18042f031737e1214455f9b8"}, 343 | {file = "pydantic-1.10.21-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:662bf5ce3c9b1cef32a32a2f4debe00d2f4839fefbebe1d6956e681122a9c839"}, 344 | {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98737c3ab5a2f8a85f2326eebcd214510f898881a290a7939a45ec294743c875"}, 345 | {file = "pydantic-1.10.21-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0bb58bbe65a43483d49f66b6c8474424d551a3fbe8a7796c42da314bac712738"}, 346 | {file = "pydantic-1.10.21-cp313-cp313-win_amd64.whl", hash = "sha256:e622314542fb48542c09c7bd1ac51d71c5632dd3c92dc82ede6da233f55f4848"}, 347 | {file = "pydantic-1.10.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d356aa5b18ef5a24d8081f5c5beb67c0a2a6ff2a953ee38d65a2aa96526b274f"}, 348 | {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08caa8c0468172d27c669abfe9e7d96a8b1655ec0833753e117061febaaadef5"}, 349 | {file = "pydantic-1.10.21-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c677aa39ec737fec932feb68e4a2abe142682f2885558402602cd9746a1c92e8"}, 350 | {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:79577cc045d3442c4e845df53df9f9202546e2ba54954c057d253fc17cd16cb1"}, 351 | {file = "pydantic-1.10.21-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:b6b73ab347284719f818acb14f7cd80696c6fdf1bd34feee1955d7a72d2e64ce"}, 352 | {file = "pydantic-1.10.21-cp37-cp37m-win_amd64.whl", hash = "sha256:46cffa24891b06269e12f7e1ec50b73f0c9ab4ce71c2caa4ccf1fb36845e1ff7"}, 353 | {file = "pydantic-1.10.21-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:298d6f765e3c9825dfa78f24c1efd29af91c3ab1b763e1fd26ae4d9e1749e5c8"}, 354 | {file = "pydantic-1.10.21-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f2f4a2305f15eff68f874766d982114ac89468f1c2c0b97640e719cf1a078374"}, 355 | {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35b263b60c519354afb3a60107d20470dd5250b3ce54c08753f6975c406d949b"}, 356 | {file = "pydantic-1.10.21-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e23a97a6c2f2db88995496db9387cd1727acdacc85835ba8619dce826c0b11a6"}, 357 | {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3c96fed246ccc1acb2df032ff642459e4ae18b315ecbab4d95c95cfa292e8517"}, 358 | {file = "pydantic-1.10.21-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b92893ebefc0151474f682e7debb6ab38552ce56a90e39a8834734c81f37c8a9"}, 359 | {file = "pydantic-1.10.21-cp38-cp38-win_amd64.whl", hash = "sha256:b8460bc256bf0de821839aea6794bb38a4c0fbd48f949ea51093f6edce0be459"}, 360 | {file = "pydantic-1.10.21-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d387940f0f1a0adb3c44481aa379122d06df8486cc8f652a7b3b0caf08435f7"}, 361 | {file = "pydantic-1.10.21-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:266ecfc384861d7b0b9c214788ddff75a2ea123aa756bcca6b2a1175edeca0fe"}, 362 | {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61da798c05a06a362a2f8c5e3ff0341743e2818d0f530eaac0d6898f1b187f1f"}, 363 | {file = "pydantic-1.10.21-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a621742da75ce272d64ea57bd7651ee2a115fa67c0f11d66d9dcfc18c2f1b106"}, 364 | {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e3e4000cd54ef455694b8be9111ea20f66a686fc155feda1ecacf2322b115da"}, 365 | {file = "pydantic-1.10.21-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f198c8206640f4c0ef5a76b779241efb1380a300d88b1bce9bfe95a6362e674d"}, 366 | {file = "pydantic-1.10.21-cp39-cp39-win_amd64.whl", hash = "sha256:e7f0cda108b36a30c8fc882e4fc5b7eec8ef584aa43aa43694c6a7b274fb2b56"}, 367 | {file = "pydantic-1.10.21-py3-none-any.whl", hash = "sha256:db70c920cba9d05c69ad4a9e7f8e9e83011abb2c6490e561de9ae24aee44925c"}, 368 | {file = "pydantic-1.10.21.tar.gz", hash = "sha256:64b48e2b609a6c22178a56c408ee1215a7206077ecb8a193e2fda31858b2362a"}, 369 | ] 370 | 371 | [package.dependencies] 372 | typing-extensions = ">=4.2.0" 373 | 374 | [package.extras] 375 | dotenv = ["python-dotenv (>=0.10.4)"] 376 | email = ["email-validator (>=1.0.3)"] 377 | 378 | [[package]] 379 | name = "pyjwt" 380 | version = "2.4.0" 381 | description = "JSON Web Token implementation in Python" 382 | optional = false 383 | python-versions = ">=3.6" 384 | files = [ 385 | {file = "PyJWT-2.4.0-py3-none-any.whl", hash = "sha256:72d1d253f32dbd4f5c88eaf1fdc62f3a19f676ccbadb9dbc5d07e951b2b26daf"}, 386 | {file = "PyJWT-2.4.0.tar.gz", hash = "sha256:d42908208c699b3b973cbeb01a969ba6a96c821eefb1c5bfe4c390c01d67abba"}, 387 | ] 388 | 389 | [package.dependencies] 390 | cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""} 391 | 392 | [package.extras] 393 | crypto = ["cryptography (>=3.3.1)"] 394 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.3.1)", "mypy", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] 395 | docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] 396 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 397 | 398 | [[package]] 399 | name = "python-dateutil" 400 | version = "2.8.2" 401 | description = "Extensions to the standard Python datetime module" 402 | optional = false 403 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 404 | files = [ 405 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 406 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 407 | ] 408 | 409 | [package.dependencies] 410 | six = ">=1.5" 411 | 412 | [[package]] 413 | name = "python-dotenv" 414 | version = "0.20.0" 415 | description = "Read key-value pairs from a .env file and set them as environment variables" 416 | optional = false 417 | python-versions = ">=3.5" 418 | files = [ 419 | {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, 420 | {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, 421 | ] 422 | 423 | [package.extras] 424 | cli = ["click (>=5.0)"] 425 | 426 | [[package]] 427 | name = "requests" 428 | version = "2.27.1" 429 | description = "Python HTTP for Humans." 430 | optional = false 431 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 432 | files = [ 433 | {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, 434 | {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, 435 | ] 436 | 437 | [package.dependencies] 438 | certifi = ">=2017.4.17" 439 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 440 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 441 | urllib3 = ">=1.21.1,<1.27" 442 | 443 | [package.extras] 444 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 445 | use-chardet-on-py3 = ["chardet (>=3.0.2,<5)"] 446 | 447 | [[package]] 448 | name = "six" 449 | version = "1.16.0" 450 | description = "Python 2 and 3 compatibility utilities" 451 | optional = false 452 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 453 | files = [ 454 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 455 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 456 | ] 457 | 458 | [[package]] 459 | name = "sniffio" 460 | version = "1.2.0" 461 | description = "Sniff out which async library your code is running under" 462 | optional = false 463 | python-versions = ">=3.5" 464 | files = [ 465 | {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, 466 | {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, 467 | ] 468 | 469 | [[package]] 470 | name = "starlette" 471 | version = "0.19.1" 472 | description = "The little ASGI library that shines." 473 | optional = false 474 | python-versions = ">=3.6" 475 | files = [ 476 | {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, 477 | {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, 478 | ] 479 | 480 | [package.dependencies] 481 | anyio = ">=3.4.0,<5" 482 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 483 | 484 | [package.extras] 485 | full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] 486 | 487 | [[package]] 488 | name = "svix" 489 | version = "0.41.2" 490 | description = "Svix" 491 | optional = false 492 | python-versions = ">=3.6" 493 | files = [ 494 | {file = "svix-0.41.2.tar.gz", hash = "sha256:f81feae9553d5cfc5a1e76e1751b667915a4ba2b493b122d8207e48e0259947f"}, 495 | ] 496 | 497 | [package.dependencies] 498 | python-dateutil = "*" 499 | urllib3 = ">=1.25.3" 500 | 501 | [[package]] 502 | name = "typing-extensions" 503 | version = "4.2.0" 504 | description = "Backported and Experimental Type Hints for Python 3.7+" 505 | optional = false 506 | python-versions = ">=3.7" 507 | files = [ 508 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 509 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 510 | ] 511 | 512 | [[package]] 513 | name = "urllib3" 514 | version = "1.26.9" 515 | description = "HTTP library with thread-safe connection pooling, file post, and more." 516 | optional = false 517 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 518 | files = [ 519 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 520 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 521 | ] 522 | 523 | [package.extras] 524 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 525 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] 526 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 527 | 528 | [[package]] 529 | name = "uvicorn" 530 | version = "0.17.6" 531 | description = "The lightning-fast ASGI server." 532 | optional = false 533 | python-versions = ">=3.7" 534 | files = [ 535 | {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, 536 | {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, 537 | ] 538 | 539 | [package.dependencies] 540 | asgiref = ">=3.4.0" 541 | click = ">=7.0" 542 | h11 = ">=0.8" 543 | 544 | [package.extras] 545 | standard = ["PyYAML (>=5.1)", "colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchgod (>=0.6)", "websockets (>=10.0)"] 546 | 547 | [[package]] 548 | name = "vital" 549 | version = "1.1.22" 550 | description = "" 551 | optional = false 552 | python-versions = ">=3.8,<4.0" 553 | files = [ 554 | {file = "vital-1.1.22-py3-none-any.whl", hash = "sha256:a89099544a7645bae397f78e93259175592d6601c70d5262f88e582f50017b93"}, 555 | {file = "vital-1.1.22.tar.gz", hash = "sha256:33ac657656c0a44d606eb8621f6ae626239a33bdfc712c33c2428a7209b5a61b"}, 556 | ] 557 | 558 | [package.dependencies] 559 | arrow = "*" 560 | auth0-python = "*" 561 | importlib-metadata = ">=3.7.3,<4.0.0" 562 | PyJWT = ">=2.0.1,<3.0.0" 563 | requests = "*" 564 | svix = ">=0.41.2,<0.42.0" 565 | 566 | [[package]] 567 | name = "zipp" 568 | version = "3.8.0" 569 | description = "Backport of pathlib-compatible object wrapper for zip files" 570 | optional = false 571 | python-versions = ">=3.7" 572 | files = [ 573 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 574 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 575 | ] 576 | 577 | [package.extras] 578 | docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] 579 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 580 | 581 | [metadata] 582 | lock-version = "2.0" 583 | python-versions = "^3.9" 584 | content-hash = "5934591ac9d12d306a60c2a71fafb4abadb93901af7ed3f9d997050fac1ad18d" 585 | --------------------------------------------------------------------------------