tests/suite-overhall - 83% Coverage

* generate API docs with make file

* documentation

* code-gen scripts

* type() to isinstance()

* code-gen

* fix flake8 problems

* test refactor first pass

* init config

* added help, format, clean and lint

* + flake8 developer dep

* update docs

* proper api imports

* jsconfig

* group tests

* refactor settings to class for testing

* fix env errors

* change tool -> tools

* code cleanup

* sort imports

* add tools test

* lint

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-03-30 11:18:37 -08:00 committed by GitHub
commit 6a71161252
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
151 changed files with 1627 additions and 4471 deletions

6
.flake8 Normal file
View file

@ -0,0 +1,6 @@
[flake8]
ignore = [
E501 # Line Length - See Black Config in pyproject.toml
E722 # Bare Exception | Temporary
]
exclude = _all_models.py

View file

@ -0,0 +1,44 @@
import json
from mealie.app import app
from mealie.core.config import DATA_DIR
"""Script to export the ReDoc documentation page into a standalone HTML file."""
HTML_TEMPLATE = """<!-- Custom HTML site displayed as the Home chapter -->
{% extends "main.html" %}
{% block tabs %}
{{ super() }}
<style>
body {
margin: 0;
padding: 0;
}
</style>
<div id="redoc-container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
<script>
var spec = MY_SPECIFIC_TEXT;
Redoc.init(spec, {}, document.getElementById("redoc-container"));
</script>
{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}
"""
HTML_PATH = DATA_DIR.parent.parent.joinpath("docs/docs/overrides/api.html")
def generate_api_docs(my_app):
with open(HTML_PATH, "w") as fd:
text = HTML_TEMPLATE.replace("MY_SPECIFIC_TEXT", json.dumps(my_app.openapi()))
fd.write(text)
if __name__ == "__main__":
generate_api_docs(app)

View file

@ -0,0 +1,83 @@
import json
import re
from pathlib import Path
from typing import Optional
import slugify
from jinja2 import Template
from mealie.app import app
from pydantic import BaseModel
CWD = Path(__file__).parent
OUT_FILE = CWD.joinpath("output", "app_routes.py")
code_template = """
class AppRoutes:
def __init__(self) -> None:
self.prefix = '{{paths.prefix}}'
{% for path in paths.static_paths %}
self.{{ path.router_slug }} = "{{path.prefix}}{{ path.route }}"{% endfor %}
{% for path in paths.function_paths %}
def {{path.router_slug}}(self, {{path.var|join(", ")}}):
return f"{self.prefix}{{ path.route }}"
{% endfor %}
"""
def get_variables(path):
path = path.replace("/", " ")
print(path)
var = re.findall(r" \{.*\}", path)
print(var)
if var:
return [v.replace("{", "").replace("}", "") for v in var]
else:
return None
class RouteObject:
def __init__(self, route_string) -> None:
self.prefix = "/" + route_string.split("/")[1]
self.route = route_string.replace(self.prefix, "")
self.parts = route_string.split("/")[1:]
self.var = re.findall(r"\{(.*?)\}", route_string)
self.is_function = "{" in self.route
self.router_slug = slugify.slugify("_".join(self.parts[1:]), separator="_")
def __repr__(self) -> str:
return f"""Route: {self.route}
Parts: {self.parts}
Function: {self.is_function}
Var: {self.var}
Slug: {self.router_slug}
"""
def get_paths(app):
paths = []
print(json.dumps(app.openapi()))
for key, value in app.openapi().items():
if key == "paths":
for key, value in value.items():
paths.append(key)
return paths
def generate_template(app):
paths = get_paths(app)
new_paths = [RouteObject(path) for path in paths]
static_paths = [p for p in new_paths if not p.is_function]
function_paths = [p for p in new_paths if p.is_function]
template = Template(code_template)
content = template.render(paths={"prefix": "/api", "static_paths": static_paths, "function_paths": function_paths})
with open(OUT_FILE, "w") as f:
f.write(content)
if __name__ == "__main__":
generate_template(app)

View file

@ -0,0 +1,97 @@
class AppRoutes:
def __init__(self) -> None:
self.prefix = "/api"
self.users_sign_ups = "/api/users/sign-ups"
self.auth_token = "/api/auth/token"
self.auth_token_long = "/api/auth/token/long"
self.auth_refresh = "/api/auth/refresh"
self.users = "/api/users"
self.users_self = "/api/users/self"
self.groups = "/api/groups"
self.groups_self = "/api/groups/self"
self.recipes = "/api/recipes"
self.recipes_category = "/api/recipes/category"
self.recipes_tag = "/api/recipes/tag"
self.categories = "/api/categories"
self.recipes_tags = "/api/recipes/tags/"
self.recipes_create = "/api/recipes/create"
self.recipes_create_url = "/api/recipes/create-url"
self.meal_plans_all = "/api/meal-plans/all"
self.meal_plans_create = "/api/meal-plans/create"
self.meal_plans_this_week = "/api/meal-plans/this-week"
self.meal_plans_today = "/api/meal-plans/today"
self.site_settings_custom_pages = "/api/site-settings/custom-pages"
self.site_settings = "/api/site-settings"
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
self.themes = "/api/themes"
self.themes_create = "/api/themes/create"
self.backups_available = "/api/backups/available"
self.backups_export_database = "/api/backups/export/database"
self.backups_upload = "/api/backups/upload"
self.migrations = "/api/migrations"
self.debug_version = "/api/debug/version"
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
def users_sign_ups_token(self, token):
return f"{self.prefix}/users/sign-ups/{token}"
def users_id(self, id):
return f"{self.prefix}/users/{id}"
def users_id_reset_password(self, id):
return f"{self.prefix}/users/{id}/reset-password"
def users_id_image(self, id):
return f"{self.prefix}/users/{id}/image"
def users_id_password(self, id):
return f"{self.prefix}/users/{id}/password"
def groups_id(self, id):
return f"{self.prefix}/groups/{id}"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def recipes_tags_tag(self, tag):
return f"{self.prefix}/recipes/tags/{tag}"
def recipes_recipe_slug(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}"
def recipes_recipe_slug_image(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/image"
def meal_plans_plan_id(self, plan_id):
return f"{self.prefix}/meal-plans/{plan_id}"
def meal_plans_id_shopping_list(self, id):
return f"{self.prefix}/meal-plans/{id}/shopping-list"
def site_settings_custom_pages_id(self, id):
return f"{self.prefix}/site-settings/custom-pages/{id}"
def themes_theme_name(self, theme_name):
return f"{self.prefix}/themes/{theme_name}"
def backups_file_name_download(self, file_name):
return f"{self.prefix}/backups/{file_name}/download"
def backups_file_name_import(self, file_name):
return f"{self.prefix}/backups/{file_name}/import"
def backups_file_name_delete(self, file_name):
return f"{self.prefix}/backups/{file_name}/delete"
def migrations_type_file_name_import(self, type, file_name):
return f"{self.prefix}/migrations/{type}/{file_name}/import"
def migrations_type_file_name_delete(self, type, file_name):
return f"{self.prefix}/migrations/{type}/{file_name}/delete"
def migrations_type_upload(self, type):
return f"{self.prefix}/migrations/{type}/upload"
def debug_log_num(self, num):
return f"{self.prefix}/debug/log/{num}"

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View file

@ -14,25 +14,46 @@ const backupURLs = {
};
export default {
/**
* Request all backups available on the server
* @returns {Array} List of Available Backups
*/
async requestAvailable() {
let response = await apiReq.get(backupURLs.available);
return response.data;
},
/**
* Calls for importing a file on the server
* @param {string} fileName
* @param {object} data
* @returns A report containing status of imported items
*/
async import(fileName, data) {
let response = await apiReq.post(backupURLs.importBackup(fileName), data);
store.dispatch("requestRecentRecipes");
return response;
},
/**
* Removes a file from the server
* @param {string} fileName
*/
async delete(fileName) {
await apiReq.delete(backupURLs.deleteBackup(fileName));
},
async create(data) {
let response = apiReq.post(backupURLs.createBackup, data);
/**
* Creates a backup on the serve given a set of options
* @param {object} data
* @returns
*/
async create(options) {
let response = apiReq.post(backupURLs.createBackup, options);
return response;
},
/**
* Downloads a file from the server. I don't actually think this is used?
* @param {string} fileName
* @returns Download URL
*/
async download(fileName) {
let response = await apiReq.get(backupURLs.downloadBackup(fileName));
return response.data;

View file

@ -12,7 +12,10 @@ import signUps from "./signUps";
import groups from "./groups";
import siteSettings from "./siteSettings";
export default {
/**
* The main object namespace for interacting with the backend database
*/
export const api = {
recipes: recipe,
siteSettings: siteSettings,
backups: backup,

View file

@ -19,6 +19,11 @@ const recipeURLs = {
};
export default {
/**
* Create a Recipe by URL
* @param {string} recipeURL
* @returns {string} Recipe Slug
*/
async createByURL(recipeURL) {
let response = await apiReq.post(recipeURLs.createByURL, {
url: recipeURL,

View file

@ -1,7 +1,7 @@
import { apiReq } from "./api-utils";
export default {
// import api from "@/api";
// import { api } from "@/api";
async uploadFile(url, fileObject) {
let response = await apiReq.post(url, fileObject, {
headers: {

View file

@ -106,7 +106,7 @@
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
import { user } from "@/mixins/user";
import api from "@/api";
import { api } from "@/api";
import axios from "axios";
export default {
mixins: [validators, initials, user],

View file

@ -39,7 +39,7 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
export default {
props: {

View file

@ -38,7 +38,7 @@
<script>
import ImportDialog from "./ImportDialog";
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
export default {
props: {

View file

@ -43,7 +43,7 @@
<script>
import ImportOptions from "@/components/Admin/Backup/ImportOptions";
import api from "@/api";
import { api } from "@/api";
export default {
components: { ImportOptions },
data() {

View file

@ -42,7 +42,7 @@
<script>
const NEW_PAGE_EVENT = "refresh-page";
import api from "@/api";
import { api } from "@/api";
import CategorySelector from "@/components/FormHelpers/CategorySelector";
export default {
components: {

View file

@ -64,7 +64,7 @@
<script>
import draggable from "vuedraggable";
import CreatePageDialog from "@/components/Admin/General/CreatePageDialog";
import api from "@/api";
import { api } from "@/api";
export default {
components: {
draggable,

View file

@ -129,7 +129,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import LanguageMenu from "@/components/UI/LanguageMenu";
import draggable from "vuedraggable";

View file

@ -3,7 +3,11 @@
<Confirmation
ref="deleteGroupConfirm"
:title="$t('user.confirm-group-deletion')"
:message="$t('user.are-you-sure-you-want-to-delete-the-group', { groupName:group.name })"
:message="
$t('user.are-you-sure-you-want-to-delete-the-group', {
groupName: group.name,
})
"
icon="mdi-alert"
@confirm="deleteGroup"
:width="450"
@ -13,7 +17,9 @@
<v-list dense>
<v-card-title class="py-1">{{ group.name }}</v-card-title>
<v-divider></v-divider>
<v-subheader>{{ $t('user.group-id-with-value', { groupID: group.id }) }}</v-subheader>
<v-subheader>{{
$t("user.group-id-with-value", { groupID: group.id })
}}</v-subheader>
<v-list-item-group color="primary">
<v-list-item v-for="property in groupProps" :key="property.text">
<v-list-item-icon>
@ -36,11 +42,11 @@
@click="confirmDelete"
:disabled="ableToDelete"
>
{{ $t('general.delete') }}
{{ $t("general.delete") }}
</v-btn>
<!-- Coming Soon! -->
<v-btn small color="success" disabled>
{{ $t('general.edit') }}
{{ $t("general.edit") }}
</v-btn>
</v-card-actions>
</v-card>
@ -50,7 +56,7 @@
<script>
const RENDER_EVENT = "update";
import Confirmation from "@/components/UI/Confirmation";
import api from "@/api";
import { api } from "@/api";
export default {
components: { Confirmation },
props: {
@ -94,22 +100,24 @@ export default {
buildData() {
this.groupProps = [
{
text: this.$t('user.total-users'),
text: this.$t("user.total-users"),
icon: "mdi-account",
value: this.group.users.length,
},
{
text: this.$t('user.total-mealplans'),
text: this.$t("user.total-mealplans"),
icon: "mdi-food",
value: this.group.mealplans.length,
},
{
text: this.$t('user.webhooks-enabled'),
text: this.$t("user.webhooks-enabled"),
icon: "mdi-webhook",
value: this.group.webhookEnable ? this.$t('general.yes') : this.$t('general.no'),
value: this.group.webhookEnable
? this.$t("general.yes")
: this.$t("general.no"),
},
{
text: this.$t('user.webhook-time'),
text: this.$t("user.webhook-time"),
icon: "mdi-clock-outline",
value: this.group.webhookTime,
},

View file

@ -84,7 +84,7 @@
<script>
import { validators } from "@/mixins/validators";
import api from "@/api";
import { api } from "@/api";
import GroupCard from "@/components/Admin/ManageUsers/GroupCard";
export default {
components: { GroupCard },

View file

@ -111,7 +111,7 @@
<script>
import Confirmation from "@/components/UI/Confirmation";
import api from "@/api";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },

View file

@ -145,7 +145,7 @@
<script>
import Confirmation from "@/components/UI/Confirmation";
import api from "@/api";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: { Confirmation },

View file

@ -60,7 +60,7 @@
<script>
import UploadBtn from "../../UI/UploadBtn";
import utils from "@/utils";
import api from "@/api";
import { api } from "@/api";
export default {
props: {
folder: String,

View file

@ -10,7 +10,10 @@
/>
<v-card flat outlined class="ma-2">
<v-card-text class="mb-n5 mt-n2">
<h3>{{ theme.name }} {{ current ? $t('general.current-parenthesis') : "" }}</h3>
<h3>
{{ theme.name }}
{{ current ? $t("general.current-parenthesis") : "" }}
</h3>
</v-card-text>
<v-card-text>
<v-row flex align-center>
@ -27,10 +30,14 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-btn text color="error" @click="confirmDelete"> {{$t('general.delete')}} </v-btn>
<v-btn text color="error" @click="confirmDelete">
{{ $t("general.delete") }}
</v-btn>
<v-spacer></v-spacer>
<!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> -->
<v-btn text color="success" @click="saveThemes">{{$t('general.apply')}}</v-btn>
<v-btn text color="success" @click="saveThemes">{{
$t("general.apply")
}}</v-btn>
</v-card-actions>
</v-card>
</div>
@ -38,7 +45,7 @@
<script>
import Confirmation from "@/components/UI/Confirmation";
import api from "@/api";
import { api } from "@/api";
const DELETE_EVENT = "delete";
const APPLY_EVENT = "apply";

View file

@ -14,7 +14,7 @@
<script>
import VJsoneditor from "v-jsoneditor";
import api from "@/api";
import { api } from "@/api";
export default {
components: { VJsoneditor },
data() {

View file

@ -64,7 +64,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
export default {
props: {},
data() {

View file

@ -83,7 +83,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
mixins: [validators],

View file

@ -20,7 +20,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
import MealPlanCard from "./MealPlanCard";
export default {

View file

@ -83,7 +83,7 @@
<script>
const CREATE_EVENT = "created";
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
import MealPlanCard from "./MealPlanCard";
export default {

View file

@ -52,7 +52,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
const levenshtein = require("fast-levenshtein");
export default {
data() {

View file

@ -21,7 +21,6 @@
ref="deleteRecipieConfirm"
v-on:confirm="deleteRecipe()"
/>
<v-btn class="mr-2" fab dark small color="success" @click="save">
<v-icon>mdi-content-save</v-icon>
</v-btn>
@ -37,7 +36,7 @@
</template>
<script>
import Confirmation from "../../components/UI/Confirmation";
import Confirmation from "../../components/UI/Confirmation.vue";
export default {
props: {
@ -58,6 +57,7 @@ export default {
save() {
this.$emit("save");
},
deleteRecipeConfrim() {
this.$refs.deleteRecipieConfirm.open();
},

View file

@ -254,7 +254,7 @@
<script>
import draggable from "vuedraggable";
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
import BulkAdd from "./BulkAdd";
import ExtrasEditor from "./ExtrasEditor";

View file

@ -71,7 +71,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
export default {
data() {

View file

@ -33,7 +33,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
export default {
data() {
return {

View file

@ -10,7 +10,7 @@
<script>
const UPLOAD_EVENT = "uploaded";
import api from "@/api";
import { api } from "@/api";
export default {
props: {
url: String,

View file

@ -47,7 +47,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
import ImportSummaryDialog from "@/components/Admin/Backup/ImportSummaryDialog";
import UploadBtn from "@/components/UI/UploadBtn";

View file

@ -108,7 +108,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import TimePickerDialog from "@/components/Admin/MealPlanner/TimePickerDialog";
export default {
components: {

View file

@ -44,7 +44,7 @@
<script>
import MigrationCard from "@/components/Admin/Migration/MigrationCard";
import SuccessFailureAlert from "@/components/UI/SuccessFailureAlert";
import api from "@/api";
import { api } from "@/api";
export default {
components: {
MigrationCard,

View file

@ -13,9 +13,9 @@
>
</v-progress-circular>
</span>
{{$t('settings.profile')}}
{{ $t("settings.profile") }}
<v-spacer></v-spacer>
{{$t('user.user-id-with-value', {id: user.id }) }}
{{ $t("user.user-id-with-value", { id: user.id }) }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
@ -86,7 +86,7 @@
<v-col cols="12" md="4" sm="12">
<v-card height="100%">
<v-card-title class="headline">
{{$t('user.reset-password')}}
{{ $t("user.reset-password") }}
<v-spacer></v-spacer>
</v-card-title>
<v-divider></v-divider>
@ -114,7 +114,8 @@
prepend-icon="mdi-lock"
:label="$t('user.confirm-password')"
:rules="[
password.newOne === password.newTwo || $t('user.password-must-match'),
password.newOne === password.newTwo ||
$t('user.password-must-match'),
]"
validate-on-blur
:type="showPassword ? 'text' : 'password'"
@ -145,7 +146,7 @@
<script>
// import AvatarPicker from '@/components/AvatarPicker'
import UploadBtn from "@/components/UI/UploadBtn";
import api from "@/api";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default {

View file

@ -134,7 +134,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
import ThemeCard from "@/components/Admin/Theme/ThemeCard";

View file

@ -21,7 +21,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import CardSection from "../components/UI/CardSection";
import CategorySidebar from "../components/UI/CategorySidebar";
export default {
@ -53,8 +53,8 @@ export default {
await this.$store.dispatch("requestSiteSettings");
this.siteSettings.categories.forEach(async element => {
let recipes = await this.getRecipeByCategory(element.slug);
if (recipes.recipes.length < 0 ) recipes.recipes = []
console.log(recipes)
if (recipes.recipes.length < 0) recipes.recipes = [];
console.log(recipes);
this.recipeByCategory.push(recipes);
});
},

View file

@ -85,7 +85,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
import NewMeal from "@/components/MealPlan/MealPlanNew";
import EditPlan from "@/components/MealPlan/MealPlanEditor";

View file

@ -51,7 +51,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
export default {
data() {

View file

@ -41,7 +41,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import RecipeEditor from "@/components/Recipe/RecipeEditor";
import VJsoneditor from "v-jsoneditor";

View file

@ -59,7 +59,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import utils from "@/utils";
import VJsoneditor from "v-jsoneditor";
import RecipeViewer from "@/components/Recipe/RecipeViewer";

View file

@ -13,7 +13,7 @@
</template>
<script>
import api from "@/api";
import { api } from "@/api";
import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
export default {

View file

@ -33,7 +33,7 @@
<script>
import CardSection from "@/components/UI/CardSection";
import CategorySidebar from "@/components/UI/CategorySidebar";
import api from "@/api";
import { api } from "@/api";
export default {
components: {

View file

@ -11,7 +11,7 @@ import Debug from "@/pages/Debug";
import LoginPage from "@/pages/LoginPage";
import SignUpPage from "@/pages/SignUpPage";
import ThisWeek from "@/pages/MealPlan/ThisWeek";
import api from "@/api";
import { api } from "@/api";
import Admin from "./admin";
import { store } from "../store";

View file

@ -1,6 +1,6 @@
import Vue from "vue";
import Vuex from "vuex";
import api from "@/api";
import { api } from "@/api";
import createPersistedState from "vuex-persistedstate";
import userSettings from "./modules/userSettings";
import language from "./modules/language";

View file

@ -1,4 +1,4 @@
import api from "@/api";
import { api } from "@/api";
const state = {
groups: [],

View file

@ -1,4 +1,4 @@
import api from "@/api";
import { api } from "@/api";
const state = {
showRecent: true,

View file

@ -1,4 +1,4 @@
import api from "@/api";
import { api } from "@/api";
const state = {
siteSettings: {

View file

@ -1,4 +1,4 @@
import api from "@/api";
import { api } from "@/api";
import Vuetify from "../../plugins/vuetify";
import axios from "axios";

View file

@ -1,25 +1,82 @@
setup:
define BROWSER_PYSCRIPT
import os, webbrowser, sys
from urllib.request import pathname2url
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT
define PRINT_HELP_PYSCRIPT
import re, sys
for line in sys.stdin:
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
if match:
target, help = match.groups()
print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT
BROWSER := python -c "$$BROWSER_PYSCRIPT"
help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
clean: clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
clean-pyc: ## remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +
clean-test: ## remove test and coverage artifacts
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
rm -fr .pytest_cache
test: ## run tests quickly with the default Python
poetry run pytest
format:
poetry run black .
lint: ## check style with flake8
poetry run flake8 mealie tests
setup: ## Setup Development Instance
poetry install && \
cd frontend && \
npm install && \
cd ..
backend:
backend: ## Start Mealie Backend Development Server
poetry run python mealie/db/init_db.py && \
poetry run python mealie/app.py
.PHONY: frontend
frontend:
frontend: ## Start Mealie Frontend Development Server
cd frontend && npm run serve
.PHONY: docs
docs:
docs: ## Start Mkdocs Development Server
poetry run python dev/scripts/api_docs_gen.py && \
cd docs && poetry run python -m mkdocs serve
docker-dev:
docker-dev: ## Build and Start Docker Development Stack
docker-compose -f docker-compose.dev.yml -p dev-mealie up --build
docker-prod:
docker-prod: ## Build and Start Docker Production Stack
docker-compose -p mealie up --build -d
code-gen: ## Run Code-Gen Scripts
poetry run python dev/scripts/app_routes_gen.py
coverage: ## check code coverage quickly with the default Python
poetry run pytest
coverage report -m
coverage html
$(BROWSER) htmlcov/index.html

View file

@ -3,25 +3,25 @@ from fastapi import FastAPI
from fastapi.logger import logger
# import utils.startup as startup
from mealie.core.config import APP_VERSION, PORT, docs_url, redoc_url
from mealie.core.config import APP_VERSION, settings
from mealie.routes import backup_routes, debug_routes, migration_routes, theme_routes
from mealie.routes.site_settings import all_settings
from mealie.routes.groups import groups
from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes
from mealie.routes.site_settings import all_settings
from mealie.routes.users import users
app = FastAPI(
title="Mealie",
description="A place for all your recipes",
version=APP_VERSION,
docs_url=docs_url,
redoc_url=redoc_url,
docs_url=settings.DOCS_URL,
redoc_url=settings.REDOC_URL,
)
def start_scheduler():
import mealie.services.scheduler.scheduled_jobs
import mealie.services.scheduler.scheduled_jobs # noqa: F401
def api_routers():
@ -55,7 +55,7 @@ def main():
uvicorn.run(
"app:app",
host="0.0.0.0",
port=PORT,
port=settings.API_PORT,
reload=True,
reload_dirs=["mealie"],
debug=True,

View file

@ -8,104 +8,101 @@ APP_VERSION = "v0.4.0"
DB_VERSION = "v0.4.0"
CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent
def ensure_dirs():
for dir in REQUIRED_DIRS:
dir.mkdir(parents=True, exist_ok=True)
# Register ENV
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
ENV = BASE_DIR.joinpath(".env")
dotenv.load_dotenv(ENV)
PRODUCTION = os.environ.get("ENV")
# General
PORT = int(os.getenv("mealie_port", 9000))
API = os.getenv("api_docs", True)
def determine_data_dir(production: bool) -> Path:
global CWD
if production:
return Path("/app/data")
if API:
docs_url = "/docs"
redoc_url = "/redoc"
else:
docs_url = None
redoc_url = None
# Helpful Globals
DATA_DIR = CWD.parent.parent.joinpath("dev", "data")
if PRODUCTION:
DATA_DIR = Path("/app/data")
WEB_PATH = CWD.joinpath("dist")
IMG_DIR = DATA_DIR.joinpath("img")
BACKUP_DIR = DATA_DIR.joinpath("backups")
DEBUG_DIR = DATA_DIR.joinpath("debug")
MIGRATION_DIR = DATA_DIR.joinpath("migration")
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
USER_DIR = DATA_DIR.joinpath("users")
SQLITE_DIR = DATA_DIR.joinpath("db")
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
TEMP_DIR = DATA_DIR.joinpath(".temp")
REQUIRED_DIRS = [
DATA_DIR,
IMG_DIR,
BACKUP_DIR,
DEBUG_DIR,
MIGRATION_DIR,
TEMPLATE_DIR,
SQLITE_DIR,
NEXTCLOUD_DIR,
CHOWDOWN_DIR,
RECIPE_DATA_DIR,
USER_DIR,
]
ensure_dirs()
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
return CWD.parent.parent.joinpath("dev", "data")
# DATABASE ENV
SQLITE_FILE = None
DATABASE_TYPE = os.getenv("DB_TYPE", "sqlite")
if DATABASE_TYPE == "sqlite":
USE_SQL = True
SQLITE_FILE = SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
else:
raise Exception("Unable to determine database type. Acceptible options are 'sqlite' ")
def determine_secrets() -> str:
if not PRODUCTION:
def determine_secrets(data_dir: Path, production: bool) -> str:
if not production:
return "shh-secret-test-key"
secrets_file = DATA_DIR.joinpath(".secret")
secrets_file = data_dir.joinpath(".secret")
if secrets_file.is_file():
with open(secrets_file, "r") as f:
return f.read()
else:
with open(secrets_file, "w") as f:
f.write(secrets.token_hex(32))
new_secret = secrets.token_hex(32)
f.write(new_secret)
return new_secret
SECRET = "determine_secrets()"
class AppDirectories:
def __init__(self, cwd, data_dir) -> None:
self.DATA_DIR = data_dir
self.WEB_PATH = cwd.joinpath("dist")
self.IMG_DIR = data_dir.joinpath("img")
self.BACKUP_DIR = data_dir.joinpath("backups")
self.DEBUG_DIR = data_dir.joinpath("debug")
self.MIGRATION_DIR = data_dir.joinpath("migration")
self.NEXTCLOUD_DIR = self.MIGRATION_DIR.joinpath("nextcloud")
self.CHOWDOWN_DIR = self.MIGRATION_DIR.joinpath("chowdown")
self.TEMPLATE_DIR = data_dir.joinpath("templates")
self.USER_DIR = data_dir.joinpath("users")
self.SQLITE_DIR = data_dir.joinpath("db")
self.RECIPE_DATA_DIR = data_dir.joinpath("recipes")
self.TEMP_DIR = data_dir.joinpath(".temp")
# Mongo Database
DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
self.ensure_directories()
# Database
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie")
DB_USERNAME = os.getenv("db_username", "root")
DB_PASSWORD = os.getenv("db_password", "example")
DB_HOST = os.getenv("db_host", "mongo")
DB_PORT = os.getenv("db_port", 27017)
def ensure_directories(self):
required_dirs = [
self.IMG_DIR,
self.BACKUP_DIR,
self.DEBUG_DIR,
self.MIGRATION_DIR,
self.TEMPLATE_DIR,
self.SQLITE_DIR,
self.NEXTCLOUD_DIR,
self.CHOWDOWN_DIR,
self.RECIPE_DATA_DIR,
self.USER_DIR,
]
# SFTP Email Stuff - For use Later down the line!
SFTP_USERNAME = os.getenv("sftp_username", None)
SFTP_PASSWORD = os.getenv("sftp_password", None)
for dir in required_dirs:
dir.mkdir(parents=True, exist_ok=True)
class AppSettings:
def __init__(self, app_dirs: AppDirectories) -> None:
global DB_VERSION
self.PRODUCTION = bool(os.environ.get("ENV"))
self.API_PORT = int(os.getenv("API_PORT", 9000))
self.API = os.getenv("API_DOCS", "False") == "True"
self.DOCS_URL = "/docs" if self.API else None
self.REDOC_URL = "/redoc" if self.API else None
self.SECRET = determine_secrets(app_dirs.DATA_DIR, self.PRODUCTION)
self.DATABASE_TYPE = os.getenv("DB_TYPE", "sqlite")
# Used to Set SQLite File Version
self.SQLITE_FILE = None
if self.DATABASE_TYPE == "sqlite":
self.SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath(f"mealie_{DB_VERSION}.sqlite")
else:
raise Exception("Unable to determine database type. Acceptible options are 'sqlite'")
self.DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
self.DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
# Not Used!
self.SFTP_USERNAME = os.getenv("SFTP_USERNAME", None)
self.SFTP_PASSWORD = os.getenv("SFTP_PASSWORD", None)
# General
DATA_DIR = determine_data_dir(PRODUCTION)
LOGGER_FILE = DATA_DIR.joinpath("mealie.log")
app_dirs = AppDirectories(CWD, DATA_DIR)
settings = AppSettings(app_dirs)

View file

@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from mealie.schema.user import UserInDB
from jose import jwt
from mealie.core.config import SECRET
from mealie.core.config import settings
from mealie.db.database import db
from passlib.context import CryptContext
@ -17,7 +17,7 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
else:
expire = datetime.utcnow() + timedelta(minutes=120)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET, algorithm=ALGORITHM)
return jwt.encode(to_encode, settings.SECRET, algorithm=ALGORITHM)
def authenticate_user(session, email: str, password: str) -> UserInDB:

View file

@ -118,6 +118,7 @@ class _SignUps(BaseDocument):
self.orm_mode = True
self.schema = SignUpOut
class _CustomPages(BaseDocument):
def __init__(self) -> None:
self.primary_key = "id"

View file

@ -1,11 +1,10 @@
from typing import List
from mealie.db.models.model_base import SqlAlchemyBase
from pydantic import BaseModel
from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session
from mealie.db.models.model_base import SqlAlchemyBase
class BaseDocument:
def __init__(self) -> None:
@ -21,12 +20,12 @@ class BaseDocument:
if self.orm_mode:
return [self.schema.from_orm(x) for x in session.query(self.sql_model).limit(limit).all()]
list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
# list = [x.dict() for x in session.query(self.sql_model).limit(limit).all()]
if limit == 1:
return list[0]
# if limit == 1:
# return list[0]
return list
# return list
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]:
"""Queries the database for the selected model. Restricts return responses to the
@ -40,12 +39,7 @@ class BaseDocument:
Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects
"""
return (
session.query(self.sql_model)
.options(load_only(*fields))
.limit(limit)
.all()
)
return session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
def get_all_primary_keys(self, session: Session) -> List[str]:
"""Queries the database of the selected model and returns a list
@ -75,11 +69,7 @@ class BaseDocument:
if match_key is None:
match_key = self.primary_key
return (
session.query(self.sql_model)
.filter_by(**{match_key: match_value})
.one()
)
return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
def get(self, session: Session, match_value: str, match_key: str = None, limit=1) -> BaseModel or List[BaseModel]:
"""Retrieves an entry from the database by matching a key/value pair. If no
@ -120,14 +110,10 @@ class BaseDocument:
session.add(new_document)
session.commit()
if self.orm_mode:
return self.schema.from_orm(new_document)
return new_document.dict()
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
"""Update a database entry.
Args: \n
session (Session): Database Session
match_value (str): Match "key"
@ -140,14 +126,9 @@ class BaseDocument:
entry = self._query_one(session=session, match_value=match_value)
entry.update(session=session, **new_data)
if self.orm_mode:
session.commit()
return self.schema.from_orm(entry)
return_data = entry.dict()
session.commit()
return return_data
def delete(self, session: Session, primary_key_value) -> dict:
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one()

View file

@ -1,15 +1,12 @@
from mealie.core.config import SQLITE_FILE, USE_SQL
from mealie.core.config import settings
from sqlalchemy.orm.session import Session
from mealie.db.models.db_session import sql_global_init
sql_exists = True
if USE_SQL:
sql_exists = SQLITE_FILE.is_file()
SessionLocal = sql_global_init(SQLITE_FILE)
else:
raise Exception("Cannot identify database type")
sql_exists = settings.SQLITE_FILE.is_file()
SessionLocal = sql_global_init(settings.SQLITE_FILE)
def create_session() -> Session:

View file

@ -1,12 +1,11 @@
from fastapi.logger import logger
from mealie.core.config import DEFAULT_GROUP, DEFAULT_PASSWORD
from mealie.core.config import settings
from mealie.core.security import get_password_hash
from mealie.db.database import db
from mealie.db.db_setup import create_session, sql_exists
from mealie.schema.settings import SiteSettings
from mealie.schema.theme import SiteTheme
from sqlalchemy.orm import Session
from sqlalchemy.orm.session import Session
def init_db(db: Session = None) -> None:
@ -24,20 +23,14 @@ def init_db(db: Session = None) -> None:
def default_theme_init(session: Session):
db.themes.create(session, SiteTheme().dict())
try:
logger.info("Generating default theme...")
except:
logger.info("Default Theme Exists.. skipping generation")
def default_settings_init(session: Session):
data = {"language": "en", "home_page_settings": {"categories": []}}
document = db.settings.create(session, SiteSettings().dict())
logger.info(f"Created Site Settings: \n {document}")
def default_group_init(session: Session):
default_group = {"name": DEFAULT_GROUP}
default_group = {"name": settings.DEFAULT_GROUP}
logger.info("Generating Default Group")
db.groups.create(session, default_group)
@ -46,8 +39,8 @@ def default_user_init(session: Session):
default_user = {
"full_name": "Change Me",
"email": "changeme@email.com",
"password": get_password_hash(DEFAULT_PASSWORD),
"group": DEFAULT_GROUP,
"password": get_password_hash(settings.DEFAULT_PASSWORD),
"group": settings.DEFAULT_GROUP,
"admin": True,
}

View file

@ -18,7 +18,7 @@ def sql_global_init(db_file: Path, check_thread=False):
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
import mealie.db.models._all_models
import mealie.db.models._all_models # noqa: F401
SqlAlchemyBase.metadata.create_all(engine)

View file

@ -1,7 +1,5 @@
import sqlalchemy as sa
import sqlalchemy.orm as orm
from fastapi.logger import logger
from mealie.core.config import DEFAULT_GROUP
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, group2categories
from sqlalchemy.orm.session import Session
@ -61,17 +59,3 @@ class Group(SqlAlchemyBase, BaseMixins):
if item is None:
item = session.query(Group).filter(Group.id == 1).one()
return item
@staticmethod
def create_if_not_exist(session, name: str = DEFAULT_GROUP):
try:
result = session.query(Group).filter(Group.name == name).one()
if result:
logger.info("Category exists, associating recipe")
return result
else:
logger.info("Category doesn't exists, creating tag")
return Group(name=name)
except:
logger.info("Category doesn't exists, creating category")
return Group(name=name)

View file

@ -1,47 +1,8 @@
from typing import List
import sqlalchemy.ext.declarative as dec
from sqlalchemy.orm.session import Session
SqlAlchemyBase = dec.declarative_base()
class BaseMixins:
@staticmethod
def _sql_remove_list(session: Session, list_of_tables: list, parent_id):
for table in list_of_tables:
session.query(table).filter(parent_id == parent_id).delete()
@staticmethod
def _flatten_dict(list_of_dict: List[dict]):
finalMap = {}
for d in list_of_dict:
finalMap.update(d.dict())
return finalMap
# ! Don't use!
def update_generics(func):
"""An experimental function that does the initial work of updating attributes on a class
and passing "complex" data types recuresively to an "self.update()" function if one exists.
Args:
func ([type]): [description]
"""
def wrapper(class_object, session, new_data: dict):
complex_attributed = {}
for key, value in new_data.items():
attribute = getattr(class_object, key, None)
if attribute and isinstance(attribute, SqlAlchemyBase):
attribute.update(session, value)
elif attribute:
setattr(class_object, key, value)
func(class_object, complex_attributed)
return wrapper
def _pass_on_me():
pass

View file

@ -1,5 +1,3 @@
from datetime import date
import sqlalchemy as sa
from mealie.db.models.model_base import SqlAlchemyBase

View file

@ -57,14 +57,10 @@ class Category(SqlAlchemyBase):
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Category).filter(Category.slug == test_slug).one()
result = session.query(Category).filter(Category.slug == test_slug).one_or_none()
if result:
logger.info("Category exists, associating recipe")
return result
else:
logger.info("Category doesn't exists, creating tag")
return Category(name=name)
except:
logger.info("Category doesn't exists, creating category")
return Category(name=name)

View file

@ -32,7 +32,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
cookTime = sa.Column(sa.String)
recipeYield = sa.Column(sa.String)
recipeCuisine = sa.Column(sa.String)
tool: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
tools: List[Tool] = orm.relationship("Tool", cascade="all, delete-orphan")
nutrition: Nutrition = orm.relationship("Nutrition", uselist=False, cascade="all, delete-orphan")
recipeCategory: List = orm.relationship("Category", secondary=recipes2categories, back_populates="recipes")
@ -76,7 +76,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
totalTime: str = None,
prepTime: str = None,
nutrition: dict = None,
tool: list[str] = [],
tools: list[str] = [],
performTime: str = None,
slug: str = None,
recipeCategory: List[str] = None,
@ -97,7 +97,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
else:
self.nutrition = Nutrition()
self.tool = [Tool(tool=x) for x in tool] if tool else []
self.tools = [Tool(tool=x) for x in tools] if tools else []
self.recipeYield = recipeYield
self.recipeIngredient = [RecipeIngredient(ingredient=ingr) for ingr in recipeIngredient]
@ -131,7 +131,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
recipeInstructions: List[dict] = None,
recipeCuisine: str = None,
totalTime: str = None,
tool: list[str] = [],
tools: list[str] = [],
prepTime: str = None,
performTime: str = None,
nutrition: dict = None,
@ -159,7 +159,7 @@ class RecipeModel(SqlAlchemyBase, BaseMixins):
prepTime=prepTime,
performTime=performTime,
nutrition=nutrition,
tool=tool,
tools=tools,
slug=slug,
recipeCategory=recipeCategory,
tags=tags,

View file

@ -22,7 +22,7 @@ class Tag(SqlAlchemyBase):
@validates("name")
def validate_name(self, key, name):
assert not name == ""
assert name != ""
return name
def __init__(self, name) -> None:
@ -32,16 +32,11 @@ class Tag(SqlAlchemyBase):
@staticmethod
def create_if_not_exist(session, name: str = None):
test_slug = slugify(name)
try:
result = session.query(Tag).filter(Tag.slug == test_slug).first()
result = session.query(Tag).filter(Tag.slug == test_slug).one_or_none()
if result:
logger.info("Tag exists, associating recipe")
return result
else:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)
except:
logger.info("Tag doesn't exists, creating tag")
return Tag(name=name)

View file

@ -10,6 +10,3 @@ class Tool(SqlAlchemyBase):
def __init__(self, tool) -> None:
self.tool = tool
def str(self):
return self.tool

View file

@ -1,4 +1,4 @@
from mealie.core.config import DEFAULT_GROUP
from mealie.core.config import settings
from mealie.db.models.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
@ -26,12 +26,12 @@ class User(SqlAlchemyBase, BaseMixins):
full_name,
email,
password,
group: str = DEFAULT_GROUP,
group: str = settings.DEFAULT_GROUP,
admin=False,
id=None,
) -> None:
group = group if group else DEFAULT_GROUP
group = group or settings.DEFAULT_GROUP
self.full_name = full_name
self.email = email
self.group = Group.get_ref(session, group)

View file

@ -2,7 +2,7 @@ import operator
import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.core.config import BACKUP_DIR, TEMPLATE_DIR
from mealie.core.config import app_dirs
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.backup import BackupJob, ImportJob, Imports, LocalBackup
@ -19,11 +19,11 @@ router = APIRouter(prefix="/api/backups", tags=["Backups"], dependencies=[Depend
def available_imports():
"""Returns a list of avaiable .zip files for import into Mealie."""
imports = []
for archive in BACKUP_DIR.glob("*.zip"):
for archive in app_dirs.app_dirs.BACKUP_DIR.glob("*.zip"):
backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
imports.append(backup)
templates = [template.name for template in TEMPLATE_DIR.glob("*.*")]
templates = [template.name for template in app_dirs.TEMPLATE_DIR.glob("*.*")]
imports.sort(key=operator.attrgetter("date"), reverse=True)
return Imports(imports=imports, templates=templates)
@ -55,7 +55,7 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
@router.post("/upload")
def upload_backup_file(archive: UploadFile = File(...)):
""" Upload a .zip File to later be imported into Mealie """
dest = BACKUP_DIR.joinpath(archive.filename)
dest = app_dirs.BACKUP_DIR.joinpath(archive.filename)
with dest.open("wb") as buffer:
shutil.copyfileobj(archive.file, buffer)
@ -69,7 +69,7 @@ def upload_backup_file(archive: UploadFile = File(...)):
@router.get("/{file_name}/download")
async def download_backup_file(file_name: str):
""" Upload a .zip File to later be imported into Mealie """
file = BACKUP_DIR.joinpath(file_name)
file = app_dirs.BACKUP_DIR.joinpath(file_name)
if file.is_file:
return FileResponse(file, media_type="application/octet-stream", filename=file_name)
@ -100,7 +100,7 @@ def delete_backup(file_name: str):
""" Removes a database backup from the file system """
try:
BACKUP_DIR.joinpath(file_name).unlink()
app_dirs.BACKUP_DIR.joinpath(file_name).unlink()
except:
HTTPException(
status_code=400,

View file

@ -1,7 +1,7 @@
import json
from fastapi import APIRouter, Depends
from mealie.core.config import APP_VERSION, DEBUG_DIR, LOGGER_FILE
from mealie.core.config import APP_VERSION, LOGGER_FILE, app_dirs
from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/debug", tags=["Debug"], dependencies=[Depends(get_current_user)])
@ -17,7 +17,7 @@ async def get_mealie_version():
async def get_last_recipe_json():
""" Doc Str """
with open(DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
with open(app_dirs.DEBUG_DIR.joinpath("last_recipe.json"), "r") as f:
return json.loads(f.read())

View file

@ -1,10 +1,10 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from mealie.core.config import SECRET
from mealie.core.config import settings
from mealie.db.database import db
from mealie.db.db_setup import create_session, generate_session
from mealie.schema.auth import Token, TokenData
from mealie.db.db_setup import generate_session
from mealie.schema.auth import TokenData
from mealie.schema.user import UserInDB
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
@ -18,7 +18,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET, algorithms=[ALGORITHM])
payload = jwt.decode(token, settings.SECRET, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
@ -29,6 +29,3 @@ async def get_current_user(token: str = Depends(oauth2_scheme), session=Depends(
if user is None:
raise credentials_exception
return user

View file

@ -3,7 +3,7 @@ from mealie.db.database import db
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserIn, UserInDB
from mealie.schema.user import GroupBase, GroupInDB, UpdateGroup, UserInDB
from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/groups", tags=["Groups"])

View file

@ -1,5 +1,3 @@
import datetime
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session

View file

@ -2,8 +2,8 @@ import operator
import shutil
from typing import List
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from mealie.core.config import MIGRATION_DIR
from fastapi import APIRouter, Depends, File, UploadFile
from mealie.core.config import app_dirs
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.migration import MigrationFile, Migrations
@ -20,8 +20,8 @@ def get_avaiable_nextcloud_imports():
""" Returns a list of avaiable directories that can be imported into Mealie """
response_data = []
migration_dirs = [
MIGRATION_DIR.joinpath("nextcloud"),
MIGRATION_DIR.joinpath("chowdown"),
app_dirs.MIGRATION_DIR.joinpath("nextcloud"),
app_dirs.MIGRATION_DIR.joinpath("chowdown"),
]
for directory in migration_dirs:
migration = Migrations(type=directory.stem)
@ -39,7 +39,7 @@ def get_avaiable_nextcloud_imports():
@router.post("/{type}/{file_name}/import")
def import_nextcloud_directory(type: str, file_name: str, session: Session = Depends(generate_session)):
""" Imports all the recipes in a given directory """
file_path = MIGRATION_DIR.joinpath(type, file_name)
file_path = app_dirs.MIGRATION_DIR.joinpath(type, file_name)
if type == "nextcloud":
return nextcloud_migrate(session, file_path)
elif type == "chowdown":
@ -52,7 +52,7 @@ def import_nextcloud_directory(type: str, file_name: str, session: Session = Dep
def delete_migration_data(type: str, file_name: str):
""" Removes migration data from the file system """
remove_path = MIGRATION_DIR.joinpath(type, file_name)
remove_path = app_dirs.MIGRATION_DIR.joinpath(type, file_name)
if remove_path.is_file():
remove_path.unlink()
@ -67,7 +67,7 @@ def delete_migration_data(type: str, file_name: str):
@router.post("/{type}/upload")
def upload_nextcloud_zipfile(type: str, archive: UploadFile = File(...)):
""" Upload a .zip File to later be imported into Mealie """
dir = MIGRATION_DIR.joinpath(type)
dir = app_dirs.MIGRATION_DIR.joinpath(type)
dir.mkdir(parents=True, exist_ok=True)
dest = dir.joinpath(archive.filename)

View file

@ -1,12 +1,10 @@
from mealie.routes.deps import get_current_user
from fastapi import APIRouter, Depends
from mealie.db.database import db
from mealie.db.db_setup import generate_session
from fastapi import APIRouter, Depends
from mealie.routes.deps import get_current_user
from mealie.schema.category import RecipeCategoryResponse
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session
from mealie.schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
router = APIRouter(
prefix="/api/categories",
@ -27,7 +25,9 @@ def get_all_recipes_by_category(category: str, session: Session = Depends(genera
@router.delete("/{category}")
async def delete_recipe_category(category: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)):
async def delete_recipe_category(
category: str, session: Session = Depends(generate_session), current_user=Depends(get_current_user)
):
"""Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed
from any recipes that contain it"""

View file

@ -1,5 +1,4 @@
from fastapi import APIRouter, Depends, File, Form, HTTPException
from fastapi.logger import logger
from fastapi.responses import FileResponse
from mealie.db.database import db
from mealie.db.db_setup import generate_session

View file

@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, status
from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from mealie.core import security
from mealie.core.security import authenticate_user, verify_password
from mealie.core.security import authenticate_user
from mealie.db.db_setup import generate_session
from mealie.routes.deps import get_current_user
from mealie.schema.snackbar import SnackResponse

View file

@ -4,7 +4,7 @@ from datetime import timedelta
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi.responses import FileResponse
from mealie.core import security
from mealie.core.config import DEFAULT_PASSWORD, USER_DIR
from mealie.core.config import settings, app_dirs
from mealie.core.security import get_password_hash, verify_password
from mealie.db.database import db
from mealie.db.db_setup import generate_session
@ -65,7 +65,7 @@ async def reset_user_password(
session: Session = Depends(generate_session),
):
new_password = get_password_hash(DEFAULT_PASSWORD)
new_password = get_password_hash(settings.DEFAULT_PASSWORD)
db.users.update_password(session, id, new_password)
return SnackResponse.success("Users Password Reset")
@ -92,7 +92,7 @@ async def update_user(
@router.get("/{id}/image")
async def get_user_image(id: str):
""" Returns a users profile picture """
user_dir = USER_DIR.joinpath(id)
user_dir = app_dirs.USER_DIR.joinpath(id)
for recipe_image in user_dir.glob("profile_image.*"):
return FileResponse(recipe_image)
else:
@ -109,14 +109,14 @@ async def update_user_image(
extension = profile_image.filename.split(".")[-1]
USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
app_dirs.USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
try:
[x.unlink() for x in USER_DIR.join(id).glob("profile_image.*")]
[x.unlink() for x in app_dirs.USER_DIR.join(id).glob("profile_image.*")]
except:
pass
dest = USER_DIR.joinpath(id, f"profile_image.{extension}")
dest = app_dirs.USER_DIR.joinpath(id, f"profile_image.{extension}")
with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer)
@ -160,4 +160,4 @@ async def delete_user(
if current_user.id == id or current_user.admin:
db.users.delete(session, id)
return SnackResponse.error(f"User Deleted")
return SnackResponse.error("User Deleted")

View file

@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional
class Token(BaseModel):
access_token: str
token_type: str

View file

@ -43,6 +43,7 @@ class Recipe(BaseModel):
recipeIngredient: Optional[list[str]]
recipeInstructions: Optional[list[RecipeStep]]
nutrition: Optional[Nutrition]
tools: Optional[list[str]] = []
totalTime: Optional[str] = None
prepTime: Optional[str] = None
@ -67,6 +68,7 @@ class Recipe(BaseModel):
"recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient],
"recipeCategory": [x.name for x in name_orm.recipeCategory],
"tags": [x.name for x in name_orm.tags],
"tools": [x.tool for x in name_orm.tools],
"extras": {x.key_name: x.value for x in name_orm.extras},
}

View file

@ -1,10 +0,0 @@
from pydantic import BaseModel
class WebhookJob(BaseModel):
webhook_urls: list[str] = []
webhook_time: str = "00:00"
webhook_enable: bool
class Config:
orm_mode = True

View file

@ -1,7 +1,7 @@
from typing import Optional
from fastapi_camelcase import CamelModel
from mealie.core.config import DEFAULT_GROUP
from mealie.core.config import settings
from mealie.db.models.group import Group
from mealie.db.models.users import User
from mealie.schema.category import CategoryBase
@ -40,7 +40,7 @@ class UserBase(CamelModel):
schema_extra = {
"fullName": "Change Me",
"email": "changeme@email.com",
"group": DEFAULT_GROUP,
"group": settings.DEFAULT_GROUP,
"admin": "false",
}

View file

@ -6,7 +6,7 @@ from typing import Union
from fastapi.logger import logger
from jinja2 import Template
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR, TEMPLATE_DIR
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.db.db_setup import create_session
from pydantic.main import BaseModel
@ -28,12 +28,12 @@ class ExportDatabase:
else:
export_tag = datetime.now().strftime("%Y-%b-%d")
self.main_dir = TEMP_DIR.joinpath(export_tag)
self.main_dir = app_dirs.TEMP_DIR.joinpath(export_tag)
self.img_dir = self.main_dir.joinpath("images")
self.templates_dir = self.main_dir.joinpath("templates")
try:
self.templates = [TEMPLATE_DIR.joinpath(x) for x in templates]
self.templates = [app_dirs.TEMPLATE_DIR.joinpath(x) for x in templates]
except:
self.templates = False
logger.info("No Jinja2 Templates Registered for Export")
@ -65,7 +65,7 @@ class ExportDatabase:
f.write(content)
def export_images(self):
for file in IMG_DIR.iterdir():
for file in app_dirs.IMG_DIR.iterdir():
shutil.copy(file, self.img_dir.joinpath(file.name))
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
@ -87,10 +87,10 @@ class ExportDatabase:
f.write(json_data)
def finish_export(self):
zip_path = BACKUP_DIR.joinpath(f"{self.main_dir.name}")
zip_path = app_dirs.BACKUP_DIR.joinpath(f"{self.main_dir.name}")
shutil.make_archive(zip_path, "zip", self.main_dir)
shutil.rmtree(TEMP_DIR)
shutil.rmtree(app_dirs.TEMP_DIR)
return str(zip_path.absolute()) + ".zip"
@ -138,10 +138,10 @@ def backup_all(
def auto_backup_job():
for backup in BACKUP_DIR.glob("Auto*.zip"):
for backup in app_dirs.BACKUP_DIR.glob("Auto*.zip"):
backup.unlink()
templates = [template for template in TEMPLATE_DIR.iterdir()]
templates = [template for template in app_dirs.TEMPLATE_DIR.iterdir()]
session = create_session()
backup_all(session=session, tag="Auto", templates=templates)
logger.info("Auto Backup Called")

View file

@ -4,7 +4,7 @@ import zipfile
from pathlib import Path
from typing import Callable, List
from mealie.core.config import BACKUP_DIR, IMG_DIR, TEMP_DIR
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport
@ -33,11 +33,11 @@ class ImportDatabase:
Exception: If the zip file does not exists an exception raise.
"""
self.session = session
self.archive = BACKUP_DIR.joinpath(zip_archive)
self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
self.force_imports = force_import
if self.archive.is_file():
self.import_dir = TEMP_DIR.joinpath("active_import")
self.import_dir = app_dirs.TEMP_DIR.joinpath("active_import")
self.import_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(self.archive, "r") as zip_ref:
@ -108,7 +108,7 @@ class ImportDatabase:
image_dir = self.import_dir.joinpath("images")
for image in image_dir.iterdir():
if image.stem in successful_imports:
shutil.copy(image, IMG_DIR)
shutil.copy(image, app_dirs.IMG_DIR)
def import_themes(self):
themes_file = self.import_dir.joinpath("themes", "themes.json")
@ -275,7 +275,7 @@ class ImportDatabase:
return import_status
def clean_up(self):
shutil.rmtree(TEMP_DIR)
shutil.rmtree(app_dirs.TEMP_DIR)
def import_database(

View file

@ -2,23 +2,23 @@ import shutil
from pathlib import Path
import requests
from mealie.core.config import IMG_DIR
from fastapi.logger import logger
from mealie.core.config import app_dirs
def read_image(recipe_slug: str) -> Path:
if IMG_DIR.joinpath(recipe_slug).is_file():
return IMG_DIR.joinpath(recipe_slug)
else:
if app_dirs.IMG_DIR.joinpath(recipe_slug).is_file():
return app_dirs.IMG_DIR.joinpath(recipe_slug)
recipe_slug = recipe_slug.split(".")[0]
for file in IMG_DIR.glob(f"{recipe_slug}*"):
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return file
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
delete_image(recipe_slug)
image_path = Path(IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))
image_path = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))
with open(image_path, "ab") as f:
f.write(file_data)
@ -27,7 +27,7 @@ def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name
def delete_image(recipe_slug: str) -> str:
recipe_slug = recipe_slug.split(".")[0]
for file in IMG_DIR.glob(f"{recipe_slug}*"):
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return file.unlink()
@ -44,7 +44,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
image_url = image_url.get("url")
filename = slug + "." + image_url.split(".")[-1]
filename = IMG_DIR.joinpath(filename)
filename = app_dirs.IMG_DIR.joinpath(filename)
try:
r = requests.get(image_url, stream=True)

View file

@ -1,13 +1,11 @@
from datetime import date, timedelta, timezone
from datetime import date, timedelta
from typing import Union
import pytz
from mealie.db.database import db
from mealie.db.db_setup import create_session
from mealie.schema.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed
from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB
from pydantic.tools import T
from sqlalchemy.orm.session import Session

View file

@ -3,7 +3,7 @@ from pathlib import Path
import yaml
from fastapi.logger import logger
from mealie.core.config import IMG_DIR, TEMP_DIR
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from mealie.utils.unzip import unpack_zip
@ -64,8 +64,8 @@ def chowdown_migrate(session: Session, zip_file: Path):
with temp_dir as dir:
chow_dir = next(Path(dir).iterdir())
image_dir = TEMP_DIR.joinpath(chow_dir, "images")
recipe_dir = TEMP_DIR.joinpath(chow_dir, "_recipes")
image_dir = app_dirs.TEMP_DIR.joinpath(chow_dir, "images")
recipe_dir = app_dirs.TEMP_DIR.joinpath(chow_dir, "_recipes")
failed_recipes = []
successful_recipes = []
@ -83,7 +83,7 @@ def chowdown_migrate(session: Session, zip_file: Path):
for image in image_dir.iterdir():
try:
if image.stem not in failed_recipes:
shutil.copy(image, IMG_DIR.joinpath(image.name))
shutil.copy(image, app_dirs.IMG_DIR.joinpath(image.name))
except Exception as inst:
logger.error(inst)
failed_images.append(image.name)

View file

@ -4,11 +4,10 @@ import shutil
import zipfile
from pathlib import Path
from mealie.core.config import IMG_DIR, MIGRATION_DIR, TEMP_DIR
from mealie.core.config import app_dirs
from mealie.db.database import db
from mealie.schema.recipe import Recipe
from mealie.services.scraper.cleaner import Cleaner
from mealie.core.config import IMG_DIR, TEMP_DIR
from mealie.db.database import db
def process_selection(selection: Path) -> Path:
@ -16,7 +15,7 @@ def process_selection(selection: Path) -> Path:
return selection
elif selection.suffix == ".zip":
with zipfile.ZipFile(selection, "r") as zip_ref:
nextcloud_dir = TEMP_DIR.joinpath("nextcloud")
nextcloud_dir = app_dirs.TEMP_DIR.joinpath("nextcloud")
nextcloud_dir.mkdir(exist_ok=False, parents=True)
zip_ref.extractall(nextcloud_dir)
return nextcloud_dir
@ -47,27 +46,27 @@ def import_recipes(recipe_dir: Path) -> Recipe:
recipe = Recipe(**recipe_data)
if image:
shutil.copy(image, IMG_DIR.joinpath(image_name))
shutil.copy(image, app_dirs.IMG_DIR.joinpath(image_name))
return recipe
def prep():
try:
shutil.rmtree(TEMP_DIR)
shutil.rmtree(app_dirs.TEMP_DIR)
except:
pass
TEMP_DIR.mkdir(exist_ok=True, parents=True)
app_dirs.TEMP_DIR.mkdir(exist_ok=True, parents=True)
def cleanup():
shutil.rmtree(TEMP_DIR)
shutil.rmtree(app_dirs.TEMP_DIR)
def migrate(session, selection: str):
prep()
MIGRATION_DIR.mkdir(exist_ok=True)
selection = MIGRATION_DIR.joinpath(selection)
app_dirs.MIGRATION_DIR.mkdir(exist_ok=True)
selection = app_dirs.MIGRATION_DIR.joinpath(selection)
nextcloud_dir = process_selection(selection)

View file

@ -44,7 +44,7 @@ class Cleaner:
@staticmethod
def category(category: str):
if type(category) == type(str):
if isinstance(category, str):
return [category]
else:
return []
@ -58,11 +58,11 @@ class Cleaner:
def image(image=None) -> str:
if not image:
return "no image"
if type(image) == list:
if isinstance(image, list):
return image[0]
elif type(image) == dict:
elif isinstance(image, dict):
return image["url"]
elif type(image) == str:
elif isinstance(image, str):
return image
else:
raise Exception(f"Unrecognised image URL format: {image}")
@ -77,11 +77,11 @@ class Cleaner:
return [{"text": Cleaner._instruction(line)} for line in instructions.splitlines() if line]
# Plain strings in a list
elif type(instructions) == list and type(instructions[0]) == str:
elif isinstance(instructions, list) and isinstance(instructions[0], str):
return [{"text": Cleaner._instruction(step)} for step in instructions]
# Dictionaries (let's assume it's a HowToStep) in a list
elif type(instructions) == list and type(instructions[0]) == dict:
elif isinstance(instructions, list) and isinstance(instructions[0], dict):
# Try List of Dictionary without "@type" or "type"
if not instructions[0].get("@type", False) and not instructions[0].get("type", False):
return [{"text": Cleaner._instruction(step["text"])} for step in instructions]
@ -106,6 +106,7 @@ class Cleaner:
if step["@type"] == "HowToStep"
]
except Exception as e:
print(e)
# Not "@type", try "type"
try:
return [
@ -121,11 +122,11 @@ class Cleaner:
@staticmethod
def _instruction(line) -> str:
l = Cleaner.html(line.strip())
clean_line = Cleaner.html(line.strip())
# Some sites erroneously escape their strings on multiple levels
while not l == (l := html.unescape(l)):
while not clean_line == (clean_line := html.unescape(clean_line)):
pass
return l
return clean_line
@staticmethod
def ingredient(ingredients: list) -> str:
@ -134,7 +135,7 @@ class Cleaner:
@staticmethod
def yield_amount(yld) -> str:
if type(yld) == list:
if isinstance(yld, list):
return yld[-1]
else:
return yld
@ -143,9 +144,9 @@ class Cleaner:
def time(time_entry):
if time_entry is None:
return None
elif type(time_entry) == datetime:
elif isinstance(time_entry, datetime):
print(time_entry)
elif type(time_entry) != str:
elif isinstance(time_entry, str):
return str(time_entry)
else:
return time_entry

View file

@ -1,11 +1,11 @@
from typing import Tuple
import extruct
from mealie.core.config import DEBUG_DIR
from mealie.core.config import app_dirs
from slugify import slugify
from w3lib.html import get_base_url
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
LAST_JSON = app_dirs.DEBUG_DIR.joinpath("last_recipe.json")
def og_field(properties: dict, field_name: str) -> str:

View file

@ -3,14 +3,14 @@ from typing import List
import requests
import scrape_schema_recipe
from mealie.core.config import DEBUG_DIR
from mealie.core.config import app_dirs
from fastapi.logger import logger
from mealie.services.image_services import scrape_image
from mealie.schema.recipe import Recipe
from mealie.services.scraper import open_graph
from mealie.services.scraper.cleaner import Cleaner
LAST_JSON = DEBUG_DIR.joinpath("last_recipe.json")
LAST_JSON = app_dirs.DEBUG_DIR.joinpath("last_recipe.json")
def create_from_url(url: str) -> Recipe:
@ -39,7 +39,7 @@ def extract_recipe_from_html(html: str, url: str) -> dict:
if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(url, python_objects=True)
except Exception as e:
# trying without python_objects
print(e)
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html)
dump_last_json(scraped_recipes)

View file

@ -1,39 +0,0 @@
import json
from mealie.core.config import DATA_DIR
"""Script to export the ReDoc documentation page into a standalone HTML file."""
HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>My Project - ReDoc</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
<style>
body {
margin: 0;
padding: 0;
}
</style>
<style data-styled="" data-styled-version="4.4.1"></style>
</head>
<body>
<div id="redoc-container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
<script>
var spec = %s;
Redoc.init(spec, {}, document.getElementById("redoc-container"));
</script>
</body>
</html>
"""
HTML_PATH = DATA_DIR.parent.joinpath("docs/docs/html/api.html")
def generate_api_docs(app):
with open(HTML_PATH, "w") as fd:
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)

View file

@ -7,7 +7,7 @@ from sqlalchemy.orm.session import Session
def post_webhooks(group: int, session: Session = None):
session = session if session else create_session()
session = session or create_session()
group_settings: GroupInDB = db.groups.get(session, group)
if not group_settings.webhook_enable:

View file

@ -2,12 +2,12 @@ import tempfile
import zipfile
from pathlib import Path
from mealie.core.config import TEMP_DIR
from mealie.core.config import app_dirs
def unpack_zip(selection: Path) -> tempfile.TemporaryDirectory:
TEMP_DIR.mkdir(parents=True, exist_ok=True)
temp_dir = tempfile.TemporaryDirectory(dir=TEMP_DIR)
app_dirs.TEMP_DIR.mkdir(parents=True, exist_ok=True)
temp_dir = tempfile.TemporaryDirectory(dir=app_dirs.TEMP_DIR)
temp_dir_path = Path(temp_dir.name)
if selection.suffix == ".zip":
with zipfile.ZipFile(selection, "r") as zip_ref:

113
poetry.lock generated
View file

@ -260,6 +260,19 @@ python-versions = ">=3.6"
pydantic = "*"
pyhumps = "*"
[[package]]
name = "flake8"
version = "3.9.0"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.7.0,<2.8.0"
pyflakes = ">=2.3.0,<2.4.0"
[[package]]
name = "future"
version = "0.18.2"
@ -620,6 +633,14 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pycodestyle"
version = "2.7.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pycparser"
version = "2.20"
@ -643,6 +664,14 @@ typing-extensions = ">=3.7.4.3"
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
[[package]]
name = "pyflakes"
version = "2.3.1"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.8.1"
@ -1125,7 +1154,7 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "688326ef0f3bf3b2d2d515b941dbca379f26b08ae83afab66fa0ec95dc2c57ce"
content-hash = "a6c10e179bc15efc30627c9793218bb944f43dce5e624a7bcabcc47545e661e8"
[metadata.files]
aiofiles = [
@ -1301,6 +1330,10 @@ fastapi = [
fastapi-camelcase = [
{file = "fastapi_camelcase-1.0.2.tar.gz", hash = "sha256:1d852149f6c9e5bb8002839a1e024050af917f1944b9d108d56468d64c6da279"},
]
flake8 = [
{file = "flake8-3.9.0-py2.py3-none-any.whl", hash = "sha256:12d05ab02614b6aee8df7c36b97d1a3b2372761222b19b58621355e82acddcff"},
{file = "flake8-3.9.0.tar.gz", hash = "sha256:78873e372b12b093da7b5e5ed302e8ad9e988b38b063b61ad937f26ca58fc5f0"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
@ -1436,39 +1469,43 @@ lunr = [
{file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
]
lxml = [
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"},
{file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a9d6bc8642e2c67db33f1247a77c53476f3a166e09067c0474facb045756087f"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:791394449e98243839fa822a637177dd42a95f4883ad3dec2a0ce6ac99fb0a9d"},
{file = "lxml-4.6.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:68a5d77e440df94011214b7db907ec8f19e439507a70c958f750c18d88f995d2"},
{file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"},
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:69a63f83e88138ab7642d8f61418cf3180a4d8cd13995df87725cb8b893e950e"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:42ebca24ba2a21065fb546f3e6bd0c58c3fe9ac298f3a320147029a4850f51a2"},
{file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"},
{file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"},
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"},
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"},
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"},
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"},
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"},
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"},
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"},
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"},
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"},
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"},
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"},
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"},
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7e9eac1e526386df7c70ef253b792a0a12dd86d833b1d329e038c7a235dfceb5"},
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ee8af0b9f7de635c61cdd5b8534b76c52cd03536f29f51151b377f76e214a1a"},
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:2e6fd1b8acd005bd71e6c94f30c055594bbd0aa02ef51a22bbfa961ab63b2d75"},
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:535332fe9d00c3cd455bd3dd7d4bacab86e2d564bdf7606079160fa6251caacf"},
{file = "lxml-4.6.2.tar.gz", hash = "sha256:cd11c7e8d21af997ee8079037fff88f16fda188a9776eb4b81c7e4c9c0a7d7fc"},
]
markdown = [
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"},
@ -1589,6 +1626,10 @@ pyasn1 = [
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pycodestyle = [
{file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
{file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
@ -1617,6 +1658,10 @@ pydantic = [
{file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"},
{file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"},
]
pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
]
pygments = [
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},

View file

@ -37,6 +37,8 @@ black = "^20.8b1"
pytest = "^6.2.1"
pytest-cov = "^2.11.0"
mkdocs-material = "^7.0.2"
flake8 = "^3.9.0"
coverage = "^5.5"
[build-system]
requires = ["poetry-core>=1.0.0"]
@ -54,3 +56,6 @@ python_functions = 'test_*'
testpaths = [
"tests",
]
[tool.coverage.report]
skip_empty = true

8
template.env Normal file
View file

@ -0,0 +1,8 @@
DEFAULT_GROUP=Home
ENV=False
API_PORT=9000
API_DOCS=True
DB_TYPE='sqlite'
DEFAULT_PASSWORD=MyPassword
SFTP_USERNAME=None
SFTP_PASSWORD=None

97
tests/app_routes.py Normal file
View file

@ -0,0 +1,97 @@
class AppRoutes:
def __init__(self) -> None:
self.prefix = "/api"
self.users_sign_ups = "/api/users/sign-ups"
self.auth_token = "/api/auth/token"
self.auth_token_long = "/api/auth/token/long"
self.auth_refresh = "/api/auth/refresh"
self.users = "/api/users"
self.users_self = "/api/users/self"
self.groups = "/api/groups"
self.groups_self = "/api/groups/self"
self.recipes = "/api/recipes"
self.recipes_category = "/api/recipes/category"
self.recipes_tag = "/api/recipes/tag"
self.categories = "/api/categories"
self.recipes_tags = "/api/recipes/tags/"
self.recipes_create = "/api/recipes/create"
self.recipes_create_url = "/api/recipes/create-url"
self.meal_plans_all = "/api/meal-plans/all"
self.meal_plans_create = "/api/meal-plans/create"
self.meal_plans_this_week = "/api/meal-plans/this-week"
self.meal_plans_today = "/api/meal-plans/today"
self.site_settings_custom_pages = "/api/site-settings/custom-pages"
self.site_settings = "/api/site-settings"
self.site_settings_webhooks_test = "/api/site-settings/webhooks/test"
self.themes = "/api/themes"
self.themes_create = "/api/themes/create"
self.backups_available = "/api/backups/available"
self.backups_export_database = "/api/backups/export/database"
self.backups_upload = "/api/backups/upload"
self.migrations = "/api/migrations"
self.debug_version = "/api/debug/version"
self.debug_last_recipe_json = "/api/debug/last-recipe-json"
def users_sign_ups_token(self, token):
return f"{self.prefix}/users/sign-ups/{token}"
def users_id(self, id):
return f"{self.prefix}/users/{id}"
def users_id_reset_password(self, id):
return f"{self.prefix}/users/{id}/reset-password"
def users_id_image(self, id):
return f"{self.prefix}/users/{id}/image"
def users_id_password(self, id):
return f"{self.prefix}/users/{id}/password"
def groups_id(self, id):
return f"{self.prefix}/groups/{id}"
def categories_category(self, category):
return f"{self.prefix}/categories/{category}"
def recipes_tags_tag(self, tag):
return f"{self.prefix}/recipes/tags/{tag}"
def recipes_recipe_slug(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}"
def recipes_recipe_slug_image(self, recipe_slug):
return f"{self.prefix}/recipes/{recipe_slug}/image"
def meal_plans_plan_id(self, plan_id):
return f"{self.prefix}/meal-plans/{plan_id}"
def meal_plans_id_shopping_list(self, id):
return f"{self.prefix}/meal-plans/{id}/shopping-list"
def site_settings_custom_pages_id(self, id):
return f"{self.prefix}/site-settings/custom-pages/{id}"
def themes_theme_name(self, theme_name):
return f"{self.prefix}/themes/{theme_name}"
def backups_file_name_download(self, file_name):
return f"{self.prefix}/backups/{file_name}/download"
def backups_file_name_import(self, file_name):
return f"{self.prefix}/backups/{file_name}/import"
def backups_file_name_delete(self, file_name):
return f"{self.prefix}/backups/{file_name}/delete"
def migrations_source_file_name_import(self, source, file_name):
return f"{self.prefix}/migrations/{source}/{file_name}/import"
def migrations_source_file_name_delete(self, source, file_name):
return f"{self.prefix}/migrations/{source}/{file_name}/delete"
def migrations_source_upload(self, source):
return f"{self.prefix}/migrations/{source}/upload"
def debug_log_num(self, num):
return f"{self.prefix}/debug/log/{num}"

View file

@ -3,16 +3,17 @@ import json
import requests
from fastapi.testclient import TestClient
from mealie.app import app
from mealie.core.config import DEFAULT_PASSWORD, SQLITE_DIR
from mealie.core.config import app_dirs, settings
from mealie.db.db_setup import generate_session, sql_global_init
from mealie.db.init_db import init_db
from pytest import fixture
from tests.app_routes import AppRoutes
from tests.test_config import TEST_DATA
from tests.utils.recipe_data import build_recipe_store, get_raw_no_image, get_raw_recipe
SQLITE_FILE = SQLITE_DIR.joinpath("test.db")
SQLITE_FILE = app_dirs.SQLITE_DIR.joinpath("test.db")
SQLITE_FILE.unlink(missing_ok=True)
TOKEN_URL = "/api/auth/token"
TestSessionLocal = sql_global_init(SQLITE_FILE, check_thread=False)
@ -37,16 +38,36 @@ def api_client():
SQLITE_FILE.unlink()
@fixture(scope="session")
def api_routes():
return AppRoutes()
@fixture(scope="session")
def test_image():
return TEST_DATA.joinpath("test_image.jpg")
@fixture(scope="session")
def token(api_client: requests):
form_data = {"username": "changeme@email.com", "password": DEFAULT_PASSWORD}
response = api_client.post(TOKEN_URL, form_data)
def token(api_client: requests, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
response = api_client.post(api_routes.auth_token, form_data)
token = json.loads(response.text).get("access_token")
return {"Authorization": f"Bearer {token}"}
@fixture(scope="session")
def raw_recipe():
return get_raw_recipe()
@fixture(scope="session")
def raw_recipe_no_image():
return get_raw_no_image()
@fixture(scope="session")
def recipe_store():
return build_recipe_store()

Some files were not shown because too many files have changed in this diff Show more