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 { export default {
/**
* Request all backups available on the server
* @returns {Array} List of Available Backups
*/
async requestAvailable() { async requestAvailable() {
let response = await apiReq.get(backupURLs.available); let response = await apiReq.get(backupURLs.available);
return response.data; 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) { async import(fileName, data) {
let response = await apiReq.post(backupURLs.importBackup(fileName), data); let response = await apiReq.post(backupURLs.importBackup(fileName), data);
store.dispatch("requestRecentRecipes"); store.dispatch("requestRecentRecipes");
return response; return response;
}, },
/**
* Removes a file from the server
* @param {string} fileName
*/
async delete(fileName) { async delete(fileName) {
await apiReq.delete(backupURLs.deleteBackup(fileName)); await apiReq.delete(backupURLs.deleteBackup(fileName));
}, },
/**
async create(data) { * Creates a backup on the serve given a set of options
let response = apiReq.post(backupURLs.createBackup, data); * @param {object} data
* @returns
*/
async create(options) {
let response = apiReq.post(backupURLs.createBackup, options);
return response; 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) { async download(fileName) {
let response = await apiReq.get(backupURLs.downloadBackup(fileName)); let response = await apiReq.get(backupURLs.downloadBackup(fileName));
return response.data; return response.data;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -145,7 +145,7 @@
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import Confirmation from "@/components/UI/Confirmation";
import api from "@/api"; import { api } from "@/api";
import { validators } from "@/mixins/validators"; import { validators } from "@/mixins/validators";
export default { export default {
components: { Confirmation }, components: { Confirmation },
@ -274,7 +274,7 @@ export default {
}, },
resetPassword() { resetPassword() {
console.log(this.activeId); console.log(this.activeId);
api.users.resetPassword(this.editedItem.id ); api.users.resetPassword(this.editedItem.id);
}, },
}, },
}; };

View file

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

View file

@ -10,7 +10,10 @@
/> />
<v-card flat outlined class="ma-2"> <v-card flat outlined class="ma-2">
<v-card-text class="mb-n5 mt-n2"> <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-card-text> <v-card-text>
<v-row flex align-center> <v-row flex align-center>
@ -27,10 +30,14 @@
</v-card-text> </v-card-text>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-actions> <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-spacer></v-spacer>
<!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> --> <!-- <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-actions>
</v-card> </v-card>
</div> </div>
@ -38,7 +45,7 @@
<script> <script>
import Confirmation from "@/components/UI/Confirmation"; import Confirmation from "@/components/UI/Confirmation";
import api from "@/api"; import { api } from "@/api";
const DELETE_EVENT = "delete"; const DELETE_EVENT = "delete";
const APPLY_EVENT = "apply"; const APPLY_EVENT = "apply";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import api from "@/api"; import { api } from "@/api";
import Vuetify from "../../plugins/vuetify"; import Vuetify from "../../plugins/vuetify";
import axios from "axios"; 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 && \ poetry install && \
cd frontend && \ cd frontend && \
npm install && \ npm install && \
cd .. cd ..
backend: backend: ## Start Mealie Backend Development Server
poetry run python mealie/db/init_db.py && \ poetry run python mealie/db/init_db.py && \
poetry run python mealie/app.py poetry run python mealie/app.py
.PHONY: frontend .PHONY: frontend
frontend: frontend: ## Start Mealie Frontend Development Server
cd frontend && npm run serve cd frontend && npm run serve
.PHONY: docs .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 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-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 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 from fastapi.logger import logger
# import utils.startup as startup # 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 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.groups import groups
from mealie.routes.mealplans import mealplans from mealie.routes.mealplans import mealplans
from mealie.routes.recipe import all_recipe_routes, category_routes, recipe_crud_routes, tag_routes 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 from mealie.routes.users import users
app = FastAPI( app = FastAPI(
title="Mealie", title="Mealie",
description="A place for all your recipes", description="A place for all your recipes",
version=APP_VERSION, version=APP_VERSION,
docs_url=docs_url, docs_url=settings.DOCS_URL,
redoc_url=redoc_url, redoc_url=settings.REDOC_URL,
) )
def start_scheduler(): def start_scheduler():
import mealie.services.scheduler.scheduled_jobs import mealie.services.scheduler.scheduled_jobs # noqa: F401
def api_routers(): def api_routers():
@ -55,7 +55,7 @@ def main():
uvicorn.run( uvicorn.run(
"app:app", "app:app",
host="0.0.0.0", host="0.0.0.0",
port=PORT, port=settings.API_PORT,
reload=True, reload=True,
reload_dirs=["mealie"], reload_dirs=["mealie"],
debug=True, debug=True,

View file

@ -8,104 +8,101 @@ APP_VERSION = "v0.4.0"
DB_VERSION = "v0.4.0" DB_VERSION = "v0.4.0"
CWD = Path(__file__).parent CWD = Path(__file__).parent
BASE_DIR = CWD.parent.parent
ENV = BASE_DIR.joinpath(".env")
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!
dotenv.load_dotenv(ENV) dotenv.load_dotenv(ENV)
PRODUCTION = os.environ.get("ENV") PRODUCTION = os.environ.get("ENV")
# General def determine_data_dir(production: bool) -> Path:
PORT = int(os.getenv("mealie_port", 9000)) global CWD
API = os.getenv("api_docs", True) if production:
return Path("/app/data")
if API: return CWD.parent.parent.joinpath("dev", "data")
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")
# DATABASE ENV def determine_secrets(data_dir: Path, production: bool) -> str:
SQLITE_FILE = None if not production:
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:
return "shh-secret-test-key" return "shh-secret-test-key"
secrets_file = DATA_DIR.joinpath(".secret") secrets_file = data_dir.joinpath(".secret")
if secrets_file.is_file(): if secrets_file.is_file():
with open(secrets_file, "r") as f: with open(secrets_file, "r") as f:
return f.read() return f.read()
else: else:
with open(secrets_file, "w") as f: 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 self.ensure_directories()
DEFAULT_GROUP = os.getenv("DEFAULT_GROUP", "Home")
DEFAULT_PASSWORD = os.getenv("DEFAULT_PASSWORD", "MyPassword")
# Database def ensure_directories(self):
MEALIE_DB_NAME = os.getenv("mealie_db_name", "mealie") required_dirs = [
DB_USERNAME = os.getenv("db_username", "root") self.IMG_DIR,
DB_PASSWORD = os.getenv("db_password", "example") self.BACKUP_DIR,
DB_HOST = os.getenv("db_host", "mongo") self.DEBUG_DIR,
DB_PORT = os.getenv("db_port", 27017) 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! for dir in required_dirs:
SFTP_USERNAME = os.getenv("sftp_username", None) dir.mkdir(parents=True, exist_ok=True)
SFTP_PASSWORD = os.getenv("sftp_password", None)
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 mealie.schema.user import UserInDB
from jose import jwt from jose import jwt
from mealie.core.config import SECRET from mealie.core.config import settings
from mealie.db.database import db from mealie.db.database import db
from passlib.context import CryptContext from passlib.context import CryptContext
@ -17,7 +17,7 @@ def create_access_token(data: dict(), expires_delta: timedelta = None) -> str:
else: else:
expire = datetime.utcnow() + timedelta(minutes=120) expire = datetime.utcnow() + timedelta(minutes=120)
to_encode.update({"exp": expire}) 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: def authenticate_user(session, email: str, password: str) -> UserInDB:

View file

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

View file

@ -1,11 +1,10 @@
from typing import List from typing import List
from mealie.db.models.model_base import SqlAlchemyBase
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy.orm import load_only from sqlalchemy.orm import load_only
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.db.models.model_base import SqlAlchemyBase
class BaseDocument: class BaseDocument:
def __init__(self) -> None: def __init__(self) -> None:
@ -21,12 +20,12 @@ class BaseDocument:
if self.orm_mode: if self.orm_mode:
return [self.schema.from_orm(x) for x in session.query(self.sql_model).limit(limit).all()] 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: # if limit == 1:
return list[0] # return list[0]
return list # return list
def get_all_limit_columns(self, session: Session, fields: List[str], limit: int = None) -> List[SqlAlchemyBase]: 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 """Queries the database for the selected model. Restricts return responses to the
@ -40,12 +39,7 @@ class BaseDocument:
Returns: Returns:
list[SqlAlchemyBase]: Returns a list of ORM objects list[SqlAlchemyBase]: Returns a list of ORM objects
""" """
return ( return session.query(self.sql_model).options(load_only(*fields)).limit(limit).all()
session.query(self.sql_model)
.options(load_only(*fields))
.limit(limit)
.all()
)
def get_all_primary_keys(self, session: Session) -> List[str]: def get_all_primary_keys(self, session: Session) -> List[str]:
"""Queries the database of the selected model and returns a list """Queries the database of the selected model and returns a list
@ -75,11 +69,7 @@ class BaseDocument:
if match_key is None: if match_key is None:
match_key = self.primary_key match_key = self.primary_key
return ( return session.query(self.sql_model).filter_by(**{match_key: match_value}).one()
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]: 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 """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.add(new_document)
session.commit() session.commit()
if self.orm_mode: return self.schema.from_orm(new_document)
return self.schema.from_orm(new_document)
return new_document.dict()
def update(self, session: Session, match_value: str, new_data: str) -> BaseModel: def update(self, session: Session, match_value: str, new_data: str) -> BaseModel:
"""Update a database entry. """Update a database entry.
Args: \n Args: \n
session (Session): Database Session session (Session): Database Session
match_value (str): Match "key" match_value (str): Match "key"
@ -140,13 +126,8 @@ class BaseDocument:
entry = self._query_one(session=session, match_value=match_value) entry = self._query_one(session=session, match_value=match_value)
entry.update(session=session, **new_data) entry.update(session=session, **new_data)
if self.orm_mode:
session.commit()
return self.schema.from_orm(entry)
return_data = entry.dict()
session.commit() session.commit()
return return_data return self.schema.from_orm(entry)
def delete(self, session: Session, primary_key_value) -> dict: def delete(self, session: Session, primary_key_value) -> dict:
result = session.query(self.sql_model).filter_by(**{self.primary_key: primary_key_value}).one() 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 sqlalchemy.orm.session import Session
from mealie.db.models.db_session import sql_global_init from mealie.db.models.db_session import sql_global_init
sql_exists = True sql_exists = True
if USE_SQL: sql_exists = settings.SQLITE_FILE.is_file()
sql_exists = SQLITE_FILE.is_file() SessionLocal = sql_global_init(settings.SQLITE_FILE)
SessionLocal = sql_global_init(SQLITE_FILE)
else:
raise Exception("Cannot identify database type")
def create_session() -> Session: def create_session() -> Session:

View file

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

View file

@ -1,7 +1,5 @@
import sqlalchemy as sa import sqlalchemy as sa
import sqlalchemy.orm as orm 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.model_base import BaseMixins, SqlAlchemyBase
from mealie.db.models.recipe.category import Category, group2categories from mealie.db.models.recipe.category import Category, group2categories
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
@ -61,17 +59,3 @@ class Group(SqlAlchemyBase, BaseMixins):
if item is None: if item is None:
item = session.query(Group).filter(Group.id == 1).one() item = session.query(Group).filter(Group.id == 1).one()
return item 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

@ -26,7 +26,7 @@ class Meal(SqlAlchemyBase):
class MealPlanModel(SqlAlchemyBase, BaseMixins): class MealPlanModel(SqlAlchemyBase, BaseMixins):
__tablename__ = "mealplan" __tablename__ = "mealplan"
uid = sa.Column(sa.Integer, primary_key=True, unique=True) #! Probably Bad? uid = sa.Column(sa.Integer, primary_key=True, unique=True) # ! Probably Bad?
startDate = sa.Column(sa.Date) startDate = sa.Column(sa.Date)
endDate = sa.Column(sa.Date) endDate = sa.Column(sa.Date)
meals: List[Meal] = orm.relationship(Meal, cascade="all, delete, delete-orphan") meals: List[Meal] = orm.relationship(Meal, cascade="all, delete, delete-orphan")

View file

@ -1,47 +1,8 @@
from typing import List
import sqlalchemy.ext.declarative as dec import sqlalchemy.ext.declarative as dec
from sqlalchemy.orm.session import Session
SqlAlchemyBase = dec.declarative_base() SqlAlchemyBase = dec.declarative_base()
class BaseMixins: class BaseMixins:
@staticmethod def _pass_on_me():
def _sql_remove_list(session: Session, list_of_tables: list, parent_id): pass
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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,6 +10,3 @@ class Tool(SqlAlchemyBase):
def __init__(self, tool) -> None: def __init__(self, tool) -> None:
self.tool = tool 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.group import Group
from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm
@ -26,12 +26,12 @@ class User(SqlAlchemyBase, BaseMixins):
full_name, full_name,
email, email,
password, password,
group: str = DEFAULT_GROUP, group: str = settings.DEFAULT_GROUP,
admin=False, admin=False,
id=None, id=None,
) -> None: ) -> None:
group = group if group else DEFAULT_GROUP group = group or settings.DEFAULT_GROUP
self.full_name = full_name self.full_name = full_name
self.email = email self.email = email
self.group = Group.get_ref(session, group) self.group = Group.get_ref(session, group)

View file

@ -2,7 +2,7 @@ import operator
import shutil import shutil
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile 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.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.backup import BackupJob, ImportJob, Imports, LocalBackup 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(): def available_imports():
"""Returns a list of avaiable .zip files for import into Mealie.""" """Returns a list of avaiable .zip files for import into Mealie."""
imports = [] 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) backup = LocalBackup(name=archive.name, date=archive.stat().st_ctime)
imports.append(backup) 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) imports.sort(key=operator.attrgetter("date"), reverse=True)
return Imports(imports=imports, templates=templates) return Imports(imports=imports, templates=templates)
@ -55,7 +55,7 @@ def export_database(data: BackupJob, session: Session = Depends(generate_session
@router.post("/upload") @router.post("/upload")
def upload_backup_file(archive: UploadFile = File(...)): def upload_backup_file(archive: UploadFile = File(...)):
""" Upload a .zip File to later be imported into Mealie """ """ 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: with dest.open("wb") as buffer:
shutil.copyfileobj(archive.file, buffer) shutil.copyfileobj(archive.file, buffer)
@ -69,7 +69,7 @@ def upload_backup_file(archive: UploadFile = File(...)):
@router.get("/{file_name}/download") @router.get("/{file_name}/download")
async def download_backup_file(file_name: str): async def download_backup_file(file_name: str):
""" Upload a .zip File to later be imported into Mealie """ """ 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: if file.is_file:
return FileResponse(file, media_type="application/octet-stream", filename=file_name) 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 """ """ Removes a database backup from the file system """
try: try:
BACKUP_DIR.joinpath(file_name).unlink() app_dirs.BACKUP_DIR.joinpath(file_name).unlink()
except: except:
HTTPException( HTTPException(
status_code=400, status_code=400,

View file

@ -1,7 +1,7 @@
import json import json
from fastapi import APIRouter, Depends 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 from mealie.routes.deps import get_current_user
router = APIRouter(prefix="/api/debug", tags=["Debug"], dependencies=[Depends(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(): async def get_last_recipe_json():
""" Doc Str """ """ 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()) return json.loads(f.read())

View file

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

View file

@ -3,7 +3,7 @@ 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.snackbar import SnackResponse 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 from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/groups", tags=["Groups"]) router = APIRouter(prefix="/api/groups", tags=["Groups"])
@ -21,7 +21,7 @@ async def get_all_groups(
@router.get("/self", response_model=GroupInDB) @router.get("/self", response_model=GroupInDB)
async def get_current_user_group( async def get_current_user_group(
current_user: UserInDB =Depends(get_current_user), current_user: UserInDB = Depends(get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns the Group Data for the Current User """ """ Returns the Group Data for the Current User """

View file

@ -1,5 +1,3 @@
import datetime
from fastapi import APIRouter, Depends 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

View file

@ -16,7 +16,7 @@ def get_shopping_list(
current_user=Depends(get_current_user), current_user=Depends(get_current_user),
): ):
#! Refactor into Single Database Call # ! Refactor into Single Database Call
mealplan = db.meals.get(session, id) mealplan = db.meals.get(session, id)
mealplan: MealPlanInDB mealplan: MealPlanInDB
slugs = [x.slug for x in mealplan.meals] slugs = [x.slug for x in mealplan.meals]

View file

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

View file

@ -73,7 +73,7 @@ def get_all_recipes_post(body: AllRecipeRequest, session: Session = Depends(gene
@router.post("/api/recipes/category") @router.post("/api/recipes/category")
def filter_by_category(categories: list, session: Session = Depends(generate_session)): def filter_by_category(categories: list, session: Session = Depends(generate_session)):
""" pass a list of categories and get a list of recipes associated with those categories """ """ pass a list of categories and get a list of recipes associated with those categories """
#! This should be refactored into a single database call, but I couldn't figure it out # ! This should be refactored into a single database call, but I couldn't figure it out
in_category = [db.categories.get(session, slugify(cat), limit=1) for cat in categories] in_category = [db.categories.get(session, slugify(cat), limit=1) for cat in categories]
in_category = [cat.get("recipes") for cat in in_category if cat] in_category = [cat.get("recipes") for cat in in_category if cat]
in_category = [item for sublist in in_category for item in sublist] in_category = [item for sublist in in_category for item in sublist]
@ -83,7 +83,7 @@ def filter_by_category(categories: list, session: Session = Depends(generate_ses
@router.post("/api/recipes/tag") @router.post("/api/recipes/tag")
async def filter_by_tags(tags: list, session: Session = Depends(generate_session)): async def filter_by_tags(tags: list, session: Session = Depends(generate_session)):
""" pass a list of tags and get a list of recipes associated with those tags""" """ pass a list of tags and get a list of recipes associated with those tags"""
#! This should be refactored into a single database call, but I couldn't figure it out # ! This should be refactored into a single database call, but I couldn't figure it out
in_tags = [db.tags.get(session, slugify(tag), limit=1) for tag in tags] in_tags = [db.tags.get(session, slugify(tag), limit=1) for tag in tags]
in_tags = [tag.get("recipes") for tag in in_tags] in_tags = [tag.get("recipes") for tag in in_tags]
in_tags = [item for sublist in in_tags for item in sublist] in_tags = [item for sublist in in_tags for item in sublist]

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.database import db
from mealie.db.db_setup import generate_session 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.category import RecipeCategoryResponse
from mealie.schema.snackbar import SnackResponse
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from mealie.schema.snackbar import SnackResponse
from mealie.schema.snackbar import SnackResponse
router = APIRouter( router = APIRouter(
prefix="/api/categories", prefix="/api/categories",
@ -27,7 +25,9 @@ def get_all_recipes_by_category(category: str, session: Session = Depends(genera
@router.delete("/{category}") @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 """Removes a recipe category from the database. Deleting a
category does not impact a recipe. The category will be removed category does not impact a recipe. The category will be removed
from any recipes that contain it""" from any recipes that contain it"""

View file

@ -1,5 +1,4 @@
from fastapi import APIRouter, Depends, File, Form, HTTPException from fastapi import APIRouter, Depends, File, Form, HTTPException
from fastapi.logger import logger
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
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

View file

@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, status
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from mealie.core import security 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.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.snackbar import SnackResponse 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 import APIRouter, Depends, File, UploadFile
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from mealie.core import security 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.core.security import get_password_hash, verify_password
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
@ -65,7 +65,7 @@ async def reset_user_password(
session: Session = Depends(generate_session), 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) db.users.update_password(session, id, new_password)
return SnackResponse.success("Users Password Reset") return SnackResponse.success("Users Password Reset")
@ -92,7 +92,7 @@ async def update_user(
@router.get("/{id}/image") @router.get("/{id}/image")
async def get_user_image(id: str): async def get_user_image(id: str):
""" Returns a users profile picture """ """ 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.*"): for recipe_image in user_dir.glob("profile_image.*"):
return FileResponse(recipe_image) return FileResponse(recipe_image)
else: else:
@ -109,14 +109,14 @@ async def update_user_image(
extension = profile_image.filename.split(".")[-1] 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: 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: except:
pass 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: with dest.open("wb") as buffer:
shutil.copyfileobj(profile_image.file, buffer) shutil.copyfileobj(profile_image.file, buffer)
@ -160,4 +160,4 @@ async def delete_user(
if current_user.id == id or current_user.admin: if current_user.id == id or current_user.admin:
db.users.delete(session, id) 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 pydantic import BaseModel
from typing import Optional from typing import Optional
class Token(BaseModel): class Token(BaseModel):
access_token: str access_token: str
token_type: str token_type: str

View file

@ -43,6 +43,7 @@ class Recipe(BaseModel):
recipeIngredient: Optional[list[str]] recipeIngredient: Optional[list[str]]
recipeInstructions: Optional[list[RecipeStep]] recipeInstructions: Optional[list[RecipeStep]]
nutrition: Optional[Nutrition] nutrition: Optional[Nutrition]
tools: Optional[list[str]] = []
totalTime: Optional[str] = None totalTime: Optional[str] = None
prepTime: Optional[str] = None prepTime: Optional[str] = None
@ -67,6 +68,7 @@ class Recipe(BaseModel):
"recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient], "recipeIngredient": [x.ingredient for x in name_orm.recipeIngredient],
"recipeCategory": [x.name for x in name_orm.recipeCategory], "recipeCategory": [x.name for x in name_orm.recipeCategory],
"tags": [x.name for x in name_orm.tags], "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}, "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 typing import Optional
from fastapi_camelcase import CamelModel 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.group import Group
from mealie.db.models.users import User from mealie.db.models.users import User
from mealie.schema.category import CategoryBase from mealie.schema.category import CategoryBase
@ -40,7 +40,7 @@ class UserBase(CamelModel):
schema_extra = { schema_extra = {
"fullName": "Change Me", "fullName": "Change Me",
"email": "changeme@email.com", "email": "changeme@email.com",
"group": DEFAULT_GROUP, "group": settings.DEFAULT_GROUP,
"admin": "false", "admin": "false",
} }

View file

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

View file

@ -4,7 +4,7 @@ import zipfile
from pathlib import Path from pathlib import Path
from typing import Callable, List 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.db.database import db
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, SettingsImport, ThemeImport, UserImport 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. Exception: If the zip file does not exists an exception raise.
""" """
self.session = session self.session = session
self.archive = BACKUP_DIR.joinpath(zip_archive) self.archive = app_dirs.BACKUP_DIR.joinpath(zip_archive)
self.force_imports = force_import self.force_imports = force_import
if self.archive.is_file(): 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) self.import_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(self.archive, "r") as zip_ref: with zipfile.ZipFile(self.archive, "r") as zip_ref:
@ -108,7 +108,7 @@ class ImportDatabase:
image_dir = self.import_dir.joinpath("images") image_dir = self.import_dir.joinpath("images")
for image in image_dir.iterdir(): for image in image_dir.iterdir():
if image.stem in successful_imports: if image.stem in successful_imports:
shutil.copy(image, IMG_DIR) shutil.copy(image, app_dirs.IMG_DIR)
def import_themes(self): def import_themes(self):
themes_file = self.import_dir.joinpath("themes", "themes.json") themes_file = self.import_dir.joinpath("themes", "themes.json")
@ -131,7 +131,7 @@ class ImportDatabase:
return theme_imports return theme_imports
def import_settings(self): #! Broken def import_settings(self): # ! Broken
settings_file = self.import_dir.joinpath("settings", "settings.json") settings_file = self.import_dir.joinpath("settings", "settings.json")
settings = ImportDatabase.read_models_file(settings_file, SiteSettings) settings = ImportDatabase.read_models_file(settings_file, SiteSettings)
settings = settings[0] settings = settings[0]
@ -275,7 +275,7 @@ class ImportDatabase:
return import_status return import_status
def clean_up(self): def clean_up(self):
shutil.rmtree(TEMP_DIR) shutil.rmtree(app_dirs.TEMP_DIR)
def import_database( def import_database(

View file

@ -2,23 +2,23 @@ import shutil
from pathlib import Path from pathlib import Path
import requests import requests
from mealie.core.config import IMG_DIR
from fastapi.logger import logger from fastapi.logger import logger
from mealie.core.config import app_dirs
def read_image(recipe_slug: str) -> Path: def read_image(recipe_slug: str) -> Path:
if IMG_DIR.joinpath(recipe_slug).is_file(): if app_dirs.IMG_DIR.joinpath(recipe_slug).is_file():
return IMG_DIR.joinpath(recipe_slug) return app_dirs.IMG_DIR.joinpath(recipe_slug)
else:
recipe_slug = recipe_slug.split(".")[0] 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 return file
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name: def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
delete_image(recipe_slug) 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: with open(image_path, "ab") as f:
f.write(file_data) 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: def delete_image(recipe_slug: str) -> str:
recipe_slug = recipe_slug.split(".")[0] 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() return file.unlink()
@ -44,7 +44,7 @@ def scrape_image(image_url: str, slug: str) -> Path:
image_url = image_url.get("url") image_url = image_url.get("url")
filename = slug + "." + image_url.split(".")[-1] filename = slug + "." + image_url.split(".")[-1]
filename = IMG_DIR.joinpath(filename) filename = app_dirs.IMG_DIR.joinpath(filename)
try: try:
r = requests.get(image_url, stream=True) 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 from typing import Union
import pytz
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.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed from mealie.schema.meal import MealIn, MealOut, MealPlanIn, MealPlanInDB, MealPlanProcessed
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.schema.user import GroupInDB from mealie.schema.user import GroupInDB
from pydantic.tools import T
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
from typing import Tuple from typing import Tuple
import extruct import extruct
from mealie.core.config import DEBUG_DIR from mealie.core.config import app_dirs
from slugify import slugify from slugify import slugify
from w3lib.html import get_base_url 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: def og_field(properties: dict, field_name: str) -> str:

View file

@ -3,14 +3,14 @@ from typing import List
import requests import requests
import scrape_schema_recipe import scrape_schema_recipe
from mealie.core.config import DEBUG_DIR from mealie.core.config import app_dirs
from fastapi.logger import logger from fastapi.logger import logger
from mealie.services.image_services import scrape_image from mealie.services.image_services import scrape_image
from mealie.schema.recipe import Recipe from mealie.schema.recipe import Recipe
from mealie.services.scraper import open_graph from mealie.services.scraper import open_graph
from mealie.services.scraper.cleaner import Cleaner 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: 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: if not scraped_recipes:
scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(url, python_objects=True) scraped_recipes: List[dict] = scrape_schema_recipe.scrape_url(url, python_objects=True)
except Exception as e: except Exception as e:
# trying without python_objects print(e)
scraped_recipes: List[dict] = scrape_schema_recipe.loads(html) scraped_recipes: List[dict] = scrape_schema_recipe.loads(html)
dump_last_json(scraped_recipes) 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): 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) group_settings: GroupInDB = db.groups.get(session, group)
if not group_settings.webhook_enable: if not group_settings.webhook_enable:

View file

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

113
poetry.lock generated
View file

@ -260,6 +260,19 @@ python-versions = ">=3.6"
pydantic = "*" pydantic = "*"
pyhumps = "*" 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]] [[package]]
name = "future" name = "future"
version = "0.18.2" version = "0.18.2"
@ -620,6 +633,14 @@ category = "main"
optional = false optional = false
python-versions = "*" 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]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.20" version = "2.20"
@ -643,6 +664,14 @@ typing-extensions = ">=3.7.4.3"
dotenv = ["python-dotenv (>=0.10.4)"] dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"] 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.8.1" version = "2.8.1"
@ -1125,7 +1154,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "688326ef0f3bf3b2d2d515b941dbca379f26b08ae83afab66fa0ec95dc2c57ce" content-hash = "a6c10e179bc15efc30627c9793218bb944f43dce5e624a7bcabcc47545e661e8"
[metadata.files] [metadata.files]
aiofiles = [ aiofiles = [
@ -1301,6 +1330,10 @@ fastapi = [
fastapi-camelcase = [ fastapi-camelcase = [
{file = "fastapi_camelcase-1.0.2.tar.gz", hash = "sha256:1d852149f6c9e5bb8002839a1e024050af917f1944b9d108d56468d64c6da279"}, {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 = [ future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, {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"}, {file = "lunr-0.5.8.tar.gz", hash = "sha256:c4fb063b98eff775dd638b3df380008ae85e6cb1d1a24d1cd81a10ef6391c26e"},
] ]
lxml = [ lxml = [
{file = "lxml-4.6.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, {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:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, {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:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, {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:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, {file = "lxml-4.6.2-cp27-cp27m-win32.whl", hash = "sha256:fc37870d6716b137e80d19241d0e2cff7a7643b925dfa49b4c8ebd1295eb506e"},
{file = "lxml-4.6.2-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, {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:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, {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:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, {file = "lxml-4.6.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f83d281bb2a6217cd806f4cf0ddded436790e66f393e124dfe9731f6b3fb9afe"},
{file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, {file = "lxml-4.6.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:535f067002b0fd1a4e5296a8f1bf88193080ff992a195e66964ef2a6cfec5388"},
{file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, {file = "lxml-4.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:366cb750140f221523fa062d641393092813b81e15d0e25d9f7c6025f910ee80"},
{file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, {file = "lxml-4.6.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:97db258793d193c7b62d4e2586c6ed98d51086e93f9a3af2b2034af01450a74b"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, {file = "lxml-4.6.2-cp35-cp35m-win32.whl", hash = "sha256:648914abafe67f11be7d93c1a546068f8eff3c5fa938e1f94509e4a5d682b2d8"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, {file = "lxml-4.6.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4e751e77006da34643ab782e4a5cc21ea7b755551db202bc4d3a423b307db780"},
{file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, {file = "lxml-4.6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:681d75e1a38a69f1e64ab82fe4b1ed3fd758717bed735fb9aeaa124143f051af"},
{file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, {file = "lxml-4.6.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:127f76864468d6630e1b453d3ffbbd04b024c674f55cf0a30dc2595137892d37"},
{file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, {file = "lxml-4.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4fb85c447e288df535b17ebdebf0ec1cf3a3f1a8eba7e79169f4f37af43c6b98"},
{file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, {file = "lxml-4.6.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5be4a2e212bb6aa045e37f7d48e3e1e4b6fd259882ed5a00786f82e8c37ce77d"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, {file = "lxml-4.6.2-cp36-cp36m-win32.whl", hash = "sha256:8c88b599e226994ad4db29d93bc149aa1aff3dc3a4355dd5757569ba78632bdf"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, {file = "lxml-4.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:6e4183800f16f3679076dfa8abf2db3083919d7e30764a069fb66b2b9eff9939"},
{file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, {file = "lxml-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d8d3d4713f0c28bdc6c806a278d998546e8efc3498949e3ace6e117462ac0a5e"},
{file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, {file = "lxml-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8246f30ca34dc712ab07e51dc34fea883c00b7ccb0e614651e49da2c49a30711"},
{file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, {file = "lxml-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:923963e989ffbceaa210ac37afc9b906acebe945d2723e9679b643513837b089"},
{file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, {file = "lxml-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:1471cee35eba321827d7d53d104e7b8c593ea3ad376aa2df89533ce8e1b24a01"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, {file = "lxml-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:2363c35637d2d9d6f26f60a208819e7eafc4305ce39dc1d5005eccc4593331c2"},
{file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, {file = "lxml-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4822c0660c3754f1a41a655e37cb4dbbc9be3d35b125a37fab6f82d47674ebc"},
{file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, {file = "lxml-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0448576c148c129594d890265b1a83b9cd76fd1f0a6a04620753d9a6bcfd0a4d"},
{file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, {file = "lxml-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60a20bfc3bd234d54d49c388950195d23a5583d4108e1a1d47c9eef8d8c042b3"},
{file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, {file = "lxml-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2e5cc908fe43fe1aa299e58046ad66981131a66aea3129aac7770c37f590a644"},
{file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, {file = "lxml-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:50c348995b47b5a4e330362cf39fc503b4a43b14a91c34c83b955e1805c8e308"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, {file = "lxml-4.6.2-cp38-cp38-win32.whl", hash = "sha256:94d55bd03d8671686e3f012577d9caa5421a07286dd351dfef64791cf7c6c505"},
{file = "lxml-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, {file = "lxml-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:7a7669ff50f41225ca5d6ee0a1ec8413f3a0d8aa2b109f86d540887b7ec0d72a"},
{file = "lxml-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, {file = "lxml-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0bfe9bb028974a481410432dbe1b182e8191d5d40382e5b8ff39cdd2e5c5931"},
{file = "lxml-4.6.2-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, {file = "lxml-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6fd8d5903c2e53f49e99359b063df27fdf7acb89a52b6a12494208bf61345a03"},
{file = "lxml-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, {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 = [ markdown = [
{file = "Markdown-3.3.4-py3-none-any.whl", hash = "sha256:96c3ba1261de2f7547b46a00ea8463832c921d3f9d6aba3f255a6f71386db20c"}, {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-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, {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 = [ pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {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-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"},
{file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"}, {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 = [ pygments = [
{file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"}, {file = "Pygments-2.8.1-py3-none-any.whl", hash = "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"},
{file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"}, {file = "Pygments-2.8.1.tar.gz", hash = "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94"},

View file

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

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