notifications first pass

This commit is contained in:
hay-kot 2021-05-07 23:05:30 -08:00
commit 566e947ffc
12 changed files with 571 additions and 76 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -12,6 +12,9 @@ const aboutURLs = {
statistics: `${prefix}/statistics`, statistics: `${prefix}/statistics`,
events: `${prefix}/events`, events: `${prefix}/events`,
event: id => `${prefix}/events/${id}`, event: id => `${prefix}/events/${id}`,
allNotifications: `${prefix}/events/notifications`,
notification: id => `${prefix}/events/notifications/${id}`,
}; };
export const aboutAPI = { export const aboutAPI = {
@ -27,6 +30,21 @@ export const aboutAPI = {
const resposne = await apiReq.delete(aboutURLs.events); const resposne = await apiReq.delete(aboutURLs.events);
return resposne.data; return resposne.data;
}, },
async allEventNotifications() {
const response = await apiReq.get(aboutURLs.allNotifications);
return response.data;
},
async createNotification(data) {
const response = await apiReq.post(aboutURLs.allNotifications, data);
return response.data;
},
async deleteNotification(id) {
const response = await apiReq.delete(aboutURLs.notification(id));
return response.data;
},
// async getAppInfo() { // async getAppInfo() {
// const response = await apiReq.get(aboutURLs.version); // const response = await apiReq.get(aboutURLs.version);
// return response.data; // return response.data;

View file

@ -0,0 +1,198 @@
<template>
<div>
<v-card outlined class="mt-n1">
<v-card-actions>
<v-spacer></v-spacer>
<BaseDialog title-icon="mdi-bell-alert" @submit="createNotification" title="New Notification">
<template v-slot:open="{ open }">
<v-btn small color="info" @click="open">
<v-icon left>
mdi-plus
</v-icon>
Notification
</v-btn>
</template>
<v-card-text class="mt-2">
We use the <a href="https://github.com/caronc/apprise/wiki" target="_blanks"> Apprise </a> library to
generate notifications. They offer many options for services to use. Refer to their wiki for a comprehensive
guide on how to create the URL for your service. If available, selecting the type of your notification can
include extra features Here are some common choices.
<div class="d-flex justify-space-around mt-1">
<a href="https://github.com/caronc/apprise/wiki/Notify_gotify" target="_blanks"> Gotify </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_discord" target="_blanks"> Discord </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_homeassistant" target="_blanks">
Home Assistant
</a>
<a href="https://github.com/caronc/apprise/wiki/Notify_matrix" target="_blanks"> Matrix </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_pushover" target="_blanks"> Pushover </a>
</div>
<v-form>
<v-select label="Type" :items="notificationTypes" item-value="text" v-model="newNotification.type">
</v-select>
<v-text-field label="Name" v-model="newNotification.name"> </v-text-field>
<v-text-field label="Notification URL" v-model="newNotification.notificationUrl"> </v-text-field>
<v-subheader class="pa-0 mb-0">
Select the events you would like to recieve notifications for on this URL
</v-subheader>
<div class="px-3 dflex row justify-space-between mt-3">
<v-switch
class="ma-0 py-0"
v-for="(item, key, index) in newNotificationOptions"
:key="index"
v-model="newNotificationOptions[key]"
:label="key"
>
</v-switch>
</div>
</v-form>
</v-card-text>
</BaseDialog>
</v-card-actions>
<v-card-text>
<v-simple-table>
<template v-slot:default>
<thead>
<tr>
<th class="text-left">
Type
</th>
<th class="text-left">
Name
</th>
<th class="text-left">
General
</th>
<th class="text-left">
Recipe
</th>
<th class="text-left">
Backup
</th>
<th class="text-left">
Scheduled
</th>
<th class="text-left">
Migration
</th>
<th class="text-left">
Group
</th>
<th class="text-left">
User
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in notifications" :key="index">
<td>
<v-avatar size="35" class="ma-1" color="primary">
<v-icon dark v-if="getIcon(item.type).icon"> {{ getIcon(item.type).icon }}</v-icon>
<v-img v-else :src="getIcon(item.type).image"> </v-img>
</v-avatar>
{{ item.type }}
</td>
<td>
{{ item.name }}
</td>
<td>
<v-icon color="success"> {{ item.general ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.recipe ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.backup ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.scheduled ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.migration ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.group ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-icon color="success"> {{ item.user ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-btn small icon color="error" @click="deleteNotification(item.id)">
<v-icon> mdi-delete </v-icon>
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-card-text>
</v-card>
</div>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
export default {
components: {
BaseDialog,
},
data() {
return {
notifications: [],
newNotification: {
type: "General",
name: "",
notificationUrl: "",
},
newNotificationOptions: {
general: true,
recipe: true,
backup: true,
scheduled: true,
migration: true,
group: true,
user: true,
},
notificationTypes: [
{
text: "General",
icon: "mdi-webhook",
},
{
text: "Discord",
icon: "mdi-discord",
},
{
text: "Gotify",
image: "./static/gotify.png",
},
],
};
},
mounted() {
this.getAllNotifications();
},
methods: {
getIcon(textValue) {
return this.notificationTypes.find(x => x.text === textValue);
},
async getAllNotifications() {
this.notifications = await api.about.allEventNotifications();
},
async createNotification() {
await api.about.createNotification({ ...this.newNotification, ...this.newNotificationOptions });
this.getAllNotifications();
},
async deleteNotification(id) {
await api.about.deleteNotification(id);
this.getAllNotifications();
},
},
};
</script>
<style lang="scss" scoped>
</style>

View file

@ -4,6 +4,10 @@
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text> <v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
<v-tabs-slider></v-tabs-slider> <v-tabs-slider></v-tabs-slider>
<v-tab href="#event-notifications">
Notify
<v-icon>mdi-bell-alert</v-icon>
</v-tab>
<v-tab href="#category-editor"> <v-tab href="#category-editor">
{{ $t("recipe.categories") }} {{ $t("recipe.categories") }}
<v-icon>mdi-tag-multiple-outline</v-icon> <v-icon>mdi-tag-multiple-outline</v-icon>
@ -20,20 +24,23 @@
</v-tabs> </v-tabs>
<v-tabs-items v-model="tab"> <v-tabs-items v-model="tab">
<v-tab-item value="event-notifications"> <EventNotification /></v-tab-item>
<v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item> <v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item>
<v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item> <v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item>
<v-tab-item value="organize"> <RecipeOrganizer :is-tags="true" /> </v-tab-item> <v-tab-item value="organize"> <RecipeOrganizer /> </v-tab-item>
</v-tabs-items> </v-tabs-items>
</v-card> </v-card>
</div> </div>
</template> </template>
<script> <script>
import EventNotification from "./EventNotification";
import CategoryTagEditor from "./CategoryTagEditor"; import CategoryTagEditor from "./CategoryTagEditor";
import RecipeOrganizer from "./RecipeOrganizer"; import RecipeOrganizer from "./RecipeOrganizer";
export default { export default {
components: { components: {
CategoryTagEditor, CategoryTagEditor,
EventNotification,
RecipeOrganizer, RecipeOrganizer,
}, },
computed: { computed: {

View file

@ -1,7 +1,7 @@
from logging import getLogger from logging import getLogger
from mealie.db.db_base import BaseDocument from mealie.db.db_base import BaseDocument
from mealie.db.models.event import Event from mealie.db.models.event import Event, EventNotification
from mealie.db.models.group import Group from mealie.db.models.group import Group
from mealie.db.models.mealplan import MealPlanModel from mealie.db.models.mealplan import MealPlanModel
from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag
@ -10,6 +10,7 @@ from mealie.db.models.sign_up import SignUp
from mealie.db.models.theme import SiteThemeModel from mealie.db.models.theme import SiteThemeModel
from mealie.db.models.users import LongLiveToken, User from mealie.db.models.users import LongLiveToken, User
from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse
from mealie.schema.event_notifications import EventNotificationIn
from mealie.schema.events import Event as EventSchema from mealie.schema.events import Event as EventSchema
from mealie.schema.meal import MealPlanInDB from mealie.schema.meal import MealPlanInDB
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
@ -156,6 +157,13 @@ class _Events(BaseDocument):
self.schema = EventSchema self.schema = EventSchema
class _EventNotification(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"
self.sql_model = EventNotification
self.schema = EventNotificationIn
class Database: class Database:
def __init__(self) -> None: def __init__(self) -> None:
self.recipes = _Recipes() self.recipes = _Recipes()
@ -170,6 +178,7 @@ class Database:
self.groups = _Groups() self.groups = _Groups()
self.custom_pages = _CustomPages() self.custom_pages = _CustomPages()
self.events = _Events() self.events = _Events()
self.event_notifications = _EventNotification()
db = Database() db = Database()

View file

@ -1,14 +1,45 @@
import sqlalchemy as sa
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, DateTime, Integer, String
class EventNotification(SqlAlchemyBase, BaseMixins):
__tablename__ = "event_notifications"
id = Column(Integer, primary_key=True)
name = Column(String)
type = Column(String)
notification_url = Column(String)
# Event Types
general = Column(Boolean, default=False)
recipe = Column(Boolean, default=False)
backup = Column(Boolean, default=False)
scheduled = Column(Boolean, default=False)
migration = Column(Boolean, default=False)
group = Column(Boolean, default=False)
user = Column(Boolean, default=False)
def __init__(
self, name, notification_url, type, general, recipe, backup, scheduled, migration, group, user, *args, **kwargs
) -> None:
self.name = name
self.notification_url = notification_url
self.type = type
self.general = general
self.recipe = recipe
self.backup = backup
self.scheduled = scheduled
self.migration = migration
self.group = group
self.user = user
class Event(SqlAlchemyBase, BaseMixins): class Event(SqlAlchemyBase, BaseMixins):
__tablename__ = "events" __tablename__ = "events"
id = sa.Column(sa.Integer, primary_key=True) id = Column(Integer, primary_key=True)
title = sa.Column(sa.String) title = Column(String)
text = sa.Column(sa.String) text = Column(String)
time_stamp = sa.Column(sa.DateTime) time_stamp = Column(DateTime)
category = sa.Column(sa.String) category = Column(String)
def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None: def __init__(self, title, text, time_stamp, category, *args, **kwargs) -> None:
self.title = title self.title = title

View file

@ -2,27 +2,71 @@ from fastapi import APIRouter, Depends
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import generate_session from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user from mealie.routes.deps import get_current_user
from mealie.schema.event_notifications import EventNotificationIn, EventNotificationOut
from mealie.schema.events import EventsOut from mealie.schema.events import EventsOut
from mealie.schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/events", tags=["App Events"]) router = APIRouter(prefix="/events", tags=["App Events"])
@router.get("", response_model=EventsOut) @router.get("", response_model=EventsOut)
async def get_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)): async def get_events(session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)):
""" Get event from the Database """ """ Get event from the Database """
# Get Item # Get Item
return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp")) return EventsOut(total=db.events.count_all(session), events=db.events.get_all(session, order_by="time_stamp"))
@router.delete("") @router.delete("")
async def delete_events(session: Session = Depends(generate_session), current_user=Depends(get_current_user)): async def delete_events(
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Get event from the Database """ """ Get event from the Database """
# Get Item # Get Item
return db.events.delete_all(session) return db.events.delete_all(session)
@router.delete("/{id}") @router.delete("/{id}")
async def delete_event(id: int, session: Session = Depends(generate_session), current_user=Depends(get_current_user)): async def delete_event(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event from the Database """ """ Delete event from the Database """
return db.events.delete(session, id) return db.events.delete(session, id)
@router.post("/notifications")
async def create_event_notification(
event_data: EventNotificationIn,
session: Session = Depends(generate_session),
current_user: UserInDB = Depends(get_current_user),
):
""" Create event_notification in the Database """
return db.event_notifications.create(session, event_data)
@router.get("/notifications", response_model=list[EventNotificationOut])
async def get_all_event_notification(
session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Get all event_notification from the Database """
# Get Item
return db.event_notifications.get_all(session, override_schema=EventNotificationOut)
@router.put("/notifications/{id}")
async def update_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Update event_notification in the Database """
# Update Item
return {"details": "not yet implemented"}
@router.delete("/notifications/{id}")
async def delete_event_notification(
id: int, session: Session = Depends(generate_session), current_user: UserInDB = Depends(get_current_user)
):
""" Delete event_notification from the Database """
# Delete Item
return db.event_notifications.delete(session, id)

View file

@ -0,0 +1,59 @@
from enum import Enum
from typing import Optional
from fastapi_camelcase import CamelModel
class DeclaredTypes(str, Enum):
general = "General"
discord = "Discord"
gotify = "Gotify"
class EventNotificationOut(CamelModel):
id: Optional[int]
name: str = ""
type: DeclaredTypes = DeclaredTypes.general
general: bool = True
recipe: bool = True
backup: bool = True
scheduled: bool = True
migration: bool = True
group: bool = True
user: bool = True
class Config:
orm_mode = True
class EventNotificationIn(EventNotificationOut):
notification_url: str = ""
class Config:
orm_mode = True
class Discord(CamelModel):
webhook_id: str
webhook_token: str
@property
def create_url(self) -> str:
return f"discord://{self.webhook_id}/{self.webhook_token}/"
class GotifyPriority(str, Enum):
low = "low"
moderate = "moderate"
normal = "normal"
high = "high"
class Gotify(CamelModel):
hostname: str
token: str
priority: GotifyPriority = GotifyPriority.normal
@property
def create_url(self) -> str:
return f"gotifys://{self.hostname}/{self.token}/?priority={self.priority}"

View file

@ -0,0 +1,32 @@
import apprise
from mealie.schema.event_notifications import EventNotificationIn
from mealie.schema.events import Event
def post_notifications(event: Event, notification_dests=list[EventNotificationIn]):
# Create an Apprise instance
# discord = Discord(
# webhook_id="840358511842295829",
# webhook_token="EtcH6ACFM-qpHRkPw1TUTc_r8AiVMlKYhGEzlANvXj7SlGGFZt18dkYy96ayZHZ8HaI9",
# )
# Create our asset object
asset = apprise.AppriseAsset(async_mode=False)
# Create our object
apobj = apprise.Apprise(asset=asset)
# Add all of the notification services by their server url.
# A sample email notification:
for dest in notification_dests:
dest: EventNotificationIn
apobj.add(dest.notification_url)
# A sample pushbullet notification
# Then notify these services any time you desire. The below would
# notify all of the services loaded into our Apprise object.
apobj.notify(
body=event.text,
title=event.title,
)

View file

@ -1,6 +1,7 @@
from mealie.db.database import db from mealie.db.database import db
from mealie.db.db_setup import create_session from mealie.db.db_setup import create_session
from mealie.schema.events import Event, EventCategory from mealie.schema.events import Event, EventCategory
from mealie.services.event_notifications import post_notifications
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -9,6 +10,9 @@ def save_event(title, text, category, session: Session):
session = session or create_session() session = session or create_session()
db.events.create(session, event.dict()) db.events.create(session, event.dict())
notification_objects = db.event_notifications.get(session=session, match_value=True, match_key=category, limit=9999)
post_notifications(event, notification_objects)
def create_general_event(title, text, session=None): def create_general_event(title, text, session=None):
category = EventCategory.general category = EventCategory.general
@ -17,6 +21,7 @@ def create_general_event(title, text, session=None):
def create_recipe_event(title, text, session=None): def create_recipe_event(title, text, session=None):
category = EventCategory.recipe category = EventCategory.recipe
save_event(title=title, text=text, category=category, session=session) save_event(title=title, text=text, category=category, session=session)

95
poetry.lock generated
View file

@ -22,6 +22,23 @@ category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "apprise"
version = "0.9.2"
description = "Push Notifications that work with just about every platform!"
category = "main"
optional = false
python-versions = ">=2.7"
[package.dependencies]
click = ">=5.0"
cryptography = "*"
markdown = "*"
PyYAML = "*"
requests = "*"
requests-oauthlib = "*"
six = "*"
[[package]] [[package]]
name = "apscheduler" name = "apscheduler"
version = "3.7.0" version = "3.7.0"
@ -189,6 +206,25 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras] [package.extras]
toml = ["toml"] toml = ["toml"]
[[package]]
name = "cryptography"
version = "3.4.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]] [[package]]
name = "decorator" name = "decorator"
version = "5.0.7" version = "5.0.7"
@ -464,7 +500,7 @@ source = ["Cython (>=0.29.7)"]
name = "markdown" name = "markdown"
version = "3.3.4" version = "3.3.4"
description = "Python implementation of Markdown." description = "Python implementation of Markdown."
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
@ -573,6 +609,19 @@ plot = ["matplotlib"]
tgrep = ["pyparsing"] tgrep = ["pyparsing"]
twitter = ["twython"] twitter = ["twython"]
[[package]]
name = "oauthlib"
version = "3.1.0"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
rsa = ["cryptography"]
signals = ["blinker"]
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "20.9" version = "20.9"
@ -916,6 +965,21 @@ urllib3 = ">=1.21.1,<1.27"
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
[[package]]
name = "requests-oauthlib"
version = "1.3.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "4.7.2" version = "4.7.2"
@ -1172,7 +1236,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "bfdb4d3d5d69e53f16b315f993b712a703058d3f59e24644681ccc9062cf5143" content-hash = "73bac73c62e64c90a29816dde9ef1d896e8ca0b4271e67cde6ca8cc56bd87efd"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1187,6 +1251,10 @@ appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
] ]
apprise = [
{file = "apprise-0.9.2-py2.py3-none-any.whl", hash = "sha256:e3f34fcf7cd717704f04b194f6fd62719e563b7d338415467e0cd0fbd7fc5e61"},
{file = "apprise-0.9.2.tar.gz", hash = "sha256:263bd6ed81cb33f3d24c9353506921129890a654965fbe9014f7f562bf2ba9f9"},
]
apscheduler = [ apscheduler = [
{file = "APScheduler-3.7.0-py2.py3-none-any.whl", hash = "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3"}, {file = "APScheduler-3.7.0-py2.py3-none-any.whl", hash = "sha256:c06cc796d5bb9eb3c4f77727f6223476eb67749e7eea074d1587550702a7fbe3"},
{file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"}, {file = "APScheduler-3.7.0.tar.gz", hash = "sha256:1cab7f2521e107d07127b042155b632b7a1cd5e02c34be5a28ff62f77c900c6a"},
@ -1329,6 +1397,20 @@ coverage = [
{file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
] ]
cryptography = [
{file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"},
{file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"},
{file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"},
{file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"},
{file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"},
{file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"},
{file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"},
{file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"},
]
decorator = [ decorator = [
{file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"}, {file = "decorator-5.0.7-py3-none-any.whl", hash = "sha256:945d84890bb20cc4a2f4a31fc4311c0c473af65ea318617f13a7257c9a58bc98"},
{file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"}, {file = "decorator-5.0.7.tar.gz", hash = "sha256:6f201a6c4dac3d187352661f508b9364ec8091217442c9478f1f83c003a0f060"},
@ -1617,6 +1699,10 @@ nltk = [
{file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"}, {file = "nltk-3.6.2-py3-none-any.whl", hash = "sha256:240e23ab1ab159ef9940777d30c7c72d7e76d91877099218a7585370c11f6b9e"},
{file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"}, {file = "nltk-3.6.2.zip", hash = "sha256:57d556abed621ab9be225cc6d2df1edce17572efb67a3d754630c9f8381503eb"},
] ]
oauthlib = [
{file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
{file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
]
packaging = [ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
@ -1854,6 +1940,11 @@ requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
] ]
requests-oauthlib = [
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
rsa = [ rsa = [
{file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
{file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},

View file

@ -1,64 +1,65 @@
[tool.poetry] [tool.poetry]
name = "mealie" name = "mealie"
version = "0.5.0b" version = "0.5.0b"
description = "A Recipe Manager" description = "A Recipe Manager"
authors = ["Hayden <hay-kot@pm.me>"] authors = ["Hayden <hay-kot@pm.me>"]
license = "MIT" license = "MIT"
[tool.poetry.scripts] [tool.poetry.scripts]
start = "mealie.app:main" start = "mealie.app:main"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
aiofiles = "0.5.0" aiofiles = "0.5.0"
aniso8601 = "7.0.0" aniso8601 = "7.0.0"
appdirs = "1.4.4" appdirs = "1.4.4"
fastapi = "^0.63.0" fastapi = "^0.63.0"
uvicorn = {extras = ["standard"], version = "^0.13.0"} uvicorn = {extras = ["standard"], version = "^0.13.0"}
APScheduler = "^3.6.3" APScheduler = "^3.6.3"
SQLAlchemy = "^1.3.22" SQLAlchemy = "^1.3.22"
Jinja2 = "^2.11.2" Jinja2 = "^2.11.2"
python-dotenv = "^0.15.0" python-dotenv = "^0.15.0"
python-slugify = "^4.0.1" python-slugify = "^4.0.1"
requests = "^2.25.1" requests = "^2.25.1"
PyYAML = "^5.3.1" PyYAML = "^5.3.1"
extruct = "^0.12.0" extruct = "^0.12.0"
scrape-schema-recipe = "^0.1.3" scrape-schema-recipe = "^0.1.3"
python-multipart = "^0.0.5" python-multipart = "^0.0.5"
fastapi-camelcase = "^1.0.2" fastapi-camelcase = "^1.0.2"
bcrypt = "^3.2.0" bcrypt = "^3.2.0"
python-jose = "^3.2.0" python-jose = "^3.2.0"
passlib = "^1.7.4" passlib = "^1.7.4"
lxml = "4.6.2" lxml = "4.6.2"
Pillow = "^8.2.0" Pillow = "^8.2.0"
pathvalidate = "^2.4.1" pathvalidate = "^2.4.1"
apprise = "^0.9.2"
[tool.poetry.dev-dependencies]
pylint = "^2.6.0" [tool.poetry.dev-dependencies]
black = "^20.8b1" pylint = "^2.6.0"
pytest = "^6.2.1" black = "^20.8b1"
pytest-cov = "^2.11.0" pytest = "^6.2.1"
mkdocs-material = "^7.0.2" pytest-cov = "^2.11.0"
flake8 = "^3.9.0" mkdocs-material = "^7.0.2"
coverage = "^5.5" flake8 = "^3.9.0"
coverage = "^5.5"
[build-system]
requires = ["poetry-core>=1.0.0"] [build-system]
build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120 [tool.black]
line-length = 120
[tool.pytest.ini_options]
minversion = "6.0" [tool.pytest.ini_options]
addopts = "-ra -q --cov=mealie" minversion = "6.0"
python_files = 'test_*' addopts = "-ra -q --cov=mealie"
python_classes = '*Tests' python_files = 'test_*'
python_functions = 'test_*' python_classes = '*Tests'
testpaths = [ python_functions = 'test_*'
"tests", testpaths = [
] "tests",
]
[tool.coverage.report]
[tool.coverage.report]
skip_empty = true skip_empty = true