mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2026-03-13 10:32:25 +08:00
add xlsx export
This commit is contained in:
@@ -173,6 +173,9 @@ Search Fields
|
||||
~~~~~~~~~~~~~
|
||||
Defined ``menu.search_fields`` in ``menu`` will render a search form by fields.
|
||||
|
||||
Excel Export
|
||||
~~~~~~~~~~~~
|
||||
FastAPI-admin can export searched data to excel file when define ``{export : True}`` in ``menu.actions``.
|
||||
|
||||
Deployment
|
||||
==========
|
||||
|
||||
@@ -49,7 +49,8 @@ def create_app():
|
||||
name='应用',
|
||||
url='/rest/App',
|
||||
icon='fa fa-pencil',
|
||||
sort_fields=('uaid',)
|
||||
sort_fields=('uaid',),
|
||||
search_fields=('uaid',)
|
||||
),
|
||||
Menu(
|
||||
name='多对多测试',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from . import routes
|
||||
|
||||
__version__ = '0.1.6'
|
||||
__version__ = '0.1.7'
|
||||
|
||||
@@ -29,7 +29,7 @@ class QueryItem(BaseModel):
|
||||
where: dict
|
||||
with_: dict
|
||||
size: int = 10
|
||||
sort: dict
|
||||
sort: dict = {}
|
||||
|
||||
class Config:
|
||||
fields = {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from fastapi import Depends
|
||||
|
||||
from . import login, other, index
|
||||
from . import login, site, index, other
|
||||
from ..depends import jwt_required
|
||||
from ..factory import app
|
||||
|
||||
app.include_router(login.router)
|
||||
app.include_router(other.router, dependencies=[Depends(jwt_required)])
|
||||
app.include_router(other.router)
|
||||
app.include_router(site.router, dependencies=[Depends(jwt_required)])
|
||||
app.include_router(index.router, dependencies=[Depends(jwt_required)])
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
from fastapi import APIRouter
|
||||
import io
|
||||
|
||||
from ..factory import app
|
||||
import xlsxwriter
|
||||
from fastapi import APIRouter, Depends
|
||||
from starlette.responses import StreamingResponse, FileResponse
|
||||
from tortoise.contrib.pydantic import pydantic_model_creator
|
||||
from fastapi_admin.depends import QueryItem, get_model, get_query
|
||||
from fastapi_admin.factory import app
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/home',
|
||||
'/{resource}/export'
|
||||
)
|
||||
async def home():
|
||||
return {
|
||||
'title': "Welcome to REST ADMIN"
|
||||
}
|
||||
async def export(
|
||||
resource: str,
|
||||
query: QueryItem = Depends(get_query),
|
||||
model=Depends(get_model)
|
||||
):
|
||||
qs = model.all()
|
||||
if query.where:
|
||||
qs = qs.filter(**query.where)
|
||||
resource = await app.get_resource(resource)
|
||||
result = await qs
|
||||
creator = pydantic_model_creator(model, include=resource.resource_fields.keys(), exclude=model._meta.m2m_fields)
|
||||
data = map(lambda x: creator.from_orm(x).dict(), result)
|
||||
|
||||
output = io.BytesIO()
|
||||
workbook = xlsxwriter.Workbook(output)
|
||||
worksheet = workbook.add_worksheet()
|
||||
for row, item in enumerate(data):
|
||||
col = 0
|
||||
for k, v in item.items():
|
||||
if row == 0:
|
||||
worksheet.write(row, col, k)
|
||||
worksheet.write(row + 1, col, v)
|
||||
col += 1
|
||||
|
||||
@router.get(
|
||||
'/site',
|
||||
)
|
||||
async def site():
|
||||
return app.site.dict(by_alias=True, exclude_unset=True)
|
||||
workbook.close()
|
||||
output.seek(0)
|
||||
|
||||
return StreamingResponse(output)
|
||||
|
||||
21
fastapi_admin/routes/site.py
Normal file
21
fastapi_admin/routes/site.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from ..factory import app
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/home',
|
||||
)
|
||||
async def home():
|
||||
return {
|
||||
'title': "Welcome to REST ADMIN"
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
'/site',
|
||||
)
|
||||
async def site():
|
||||
return app.site.dict(by_alias=True, exclude_unset=True)
|
||||
@@ -25,7 +25,8 @@ class Menu(BaseModel):
|
||||
actions: Dict = {
|
||||
'toolbar': {
|
||||
'delete_all': True
|
||||
}
|
||||
},
|
||||
'export': True
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"cropperjs": "^1.5.6",
|
||||
"dayjs": "^1.8.16",
|
||||
"file-saver": "^2.0.2",
|
||||
"font-awesome": "^4.7.0",
|
||||
"inflection": "^1.12.0",
|
||||
"jsoneditor": "^8.6.4",
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
v-for="button in _.get(actions, 'toolbar.extra', [])"
|
||||
:key="button.label"
|
||||
v-bind="button"
|
||||
>{{button.label}}</b-btn>
|
||||
>{{button.label}}
|
||||
</b-btn>
|
||||
<b-btn
|
||||
@click="removeAll"
|
||||
class="pull-right"
|
||||
@@ -51,18 +52,18 @@
|
||||
:fields="table.searchFields"
|
||||
v-model="table.searchModel"
|
||||
>
|
||||
|
||||
<div slot="extra-buttons" class="ml-2">
|
||||
<b-button
|
||||
type="button"
|
||||
@click="searchAndExport"
|
||||
variant="success"
|
||||
v-if="_.get(actions, 'export')"
|
||||
>{{$t('actions.search_and_export')}}</b-button>
|
||||
<iframe :src="iframeSrc" style="width:0;height:0;border:none;"></iframe>
|
||||
</div>
|
||||
|
||||
<div slot="extra-buttons" class="ml-2">
|
||||
<b-button
|
||||
type="button"
|
||||
@click="searchAndExport"
|
||||
variant="success"
|
||||
v-if="_.get(actions, 'export')"
|
||||
>{{$t('actions.search_and_export')}}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form-builder>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
@@ -97,7 +98,8 @@
|
||||
:key="key"
|
||||
class="table-header"
|
||||
:class="{'text-right': ['number'].includes(field.type)}"
|
||||
>{{field.label || key}}</div>
|
||||
>{{field.label || key}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="(field, key) in table.fields" :slot="key" slot-scope="row">
|
||||
<b-data-value :field="field" :key="key" :name="key" :model="row.item" short-id/>
|
||||
@@ -112,26 +114,30 @@
|
||||
size="sm"
|
||||
v-bind="field"
|
||||
v-show="field.label"
|
||||
>{{field.label}}</b-button>
|
||||
>{{field.label}}
|
||||
</b-button>
|
||||
<b-btn
|
||||
v-if="actions.edit !== false"
|
||||
variant="success"
|
||||
size="sm"
|
||||
:to="`/rest/${uri}/${row.item[$config.primaryKey]}`"
|
||||
class="mr-1"
|
||||
>{{$t('actions.view')}}</b-btn>
|
||||
>{{$t('actions.view')}}
|
||||
</b-btn>
|
||||
<b-btn
|
||||
v-if="actions.edit !== false"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:to="`/rest/${uri}/${row.item[$config.primaryKey]}/edit`"
|
||||
class="mr-1"
|
||||
>{{$t('actions.edit')}}</b-btn>
|
||||
>{{$t('actions.edit')}}
|
||||
</b-btn>
|
||||
<b-btn
|
||||
v-if="actions.delete !== false"
|
||||
size="sm"
|
||||
@click.stop="remove(row.item[$config.primaryKey])"
|
||||
>{{$t('actions.delete')}}</b-btn>
|
||||
>{{$t('actions.delete')}}
|
||||
</b-btn>
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
@@ -151,171 +157,176 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import types from "../store/types";
|
||||
import _ from "lodash";
|
||||
import {mapState, mapGetters} from "vuex";
|
||||
import types from "../store/types";
|
||||
import _ from "lodash";
|
||||
import {saveAs} from 'file-saver';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
init: false,
|
||||
loaded: false,
|
||||
table: {},
|
||||
total: 0, //total rows
|
||||
pageLimit: 10, //display how many page buttons
|
||||
currentPage: 1,
|
||||
sortBy: this.$config.primaryKey,
|
||||
sortDesc: true,
|
||||
sortDirection: null,
|
||||
perPage: 10,
|
||||
where: {},
|
||||
iframeSrc: ""
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$route.query"(val) {
|
||||
this.applyRouteQuery();
|
||||
export default {
|
||||
components: {},
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
init: false,
|
||||
loaded: false,
|
||||
table: {},
|
||||
total: 0, //total rows
|
||||
pageLimit: 10, //display how many page buttons
|
||||
currentPage: 1,
|
||||
sortBy: this.$config.primaryKey,
|
||||
sortDesc: true,
|
||||
sortDirection: null,
|
||||
perPage: 10,
|
||||
where: {},
|
||||
iframeSrc: ""
|
||||
};
|
||||
},
|
||||
"$route.params"(val) {
|
||||
this.applyRouteQuery();
|
||||
this.fetch();
|
||||
}
|
||||
// page(val) {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(["site", "i18n", "auth"]),
|
||||
...mapGetters(["currentLanguage"]),
|
||||
columns(){
|
||||
return Object.entries(this.table.fields).map(([name, field]) => {
|
||||
return {
|
||||
key:name,
|
||||
...field,
|
||||
}
|
||||
})
|
||||
},
|
||||
populate() {
|
||||
return _(this.table.fields || {})
|
||||
.map("ref")
|
||||
.filter()
|
||||
.map(v => v.split(".").shift())
|
||||
.uniq()
|
||||
.toJSON();
|
||||
},
|
||||
actions() {
|
||||
return _.get(this.table, "fields._actions", {});
|
||||
},
|
||||
resource() {
|
||||
return this.$route.params.resource;
|
||||
},
|
||||
uri() {
|
||||
return this.resource.replace(/\./g, "/");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doSearch(params) {
|
||||
this.where = _.omitBy(params, v => v === null);
|
||||
this.$refs.table.refresh();
|
||||
// console.log(params);
|
||||
},
|
||||
searchAndExport() {
|
||||
const query = JSON.stringify({
|
||||
where: _.clone(this.table.searchModel),
|
||||
with: _.clone(this.populate)
|
||||
});
|
||||
this.iframeSrc = "";
|
||||
setTimeout(() => {
|
||||
this.iframeSrc = `${global.API_URI}${
|
||||
this.uri
|
||||
}/export?query=${query}&token=${this.$store.state.auth.token}`;
|
||||
}, 50);
|
||||
},
|
||||
applyRouteQuery() {
|
||||
const { sort = {}, page = 1, where = {} } = JSON.parse(
|
||||
this.$route.query.query || "{}"
|
||||
);
|
||||
const [sortBy, sortDesc] = Object.entries(sort).pop() || [];
|
||||
sortBy && (this.sortBy = sortBy);
|
||||
|
||||
if (sortDesc) {
|
||||
this.sortDesc = sortDesc === -1 ? true : false;
|
||||
watch: {
|
||||
"$route.query"(val) {
|
||||
this.applyRouteQuery();
|
||||
},
|
||||
"$route.params"(val) {
|
||||
this.applyRouteQuery();
|
||||
this.fetch();
|
||||
}
|
||||
this.total = page * this.perPage;
|
||||
this.currentPage = page;
|
||||
this.where = where;
|
||||
this.init = true;
|
||||
// page(val) {}
|
||||
},
|
||||
remove(id) {
|
||||
if (window.confirm("是否删除?")) {
|
||||
this.$http.delete(`${this.uri}/${id}`).then(res => {
|
||||
this.$snotify.success("删除成功");
|
||||
this.$refs.table.refresh();
|
||||
});
|
||||
}
|
||||
},
|
||||
fetchItems(ctx) {
|
||||
const query = _.merge({}, _.get(this.table, "query"), {
|
||||
page: ctx.currentPage,
|
||||
sort: { [ctx.sortBy]: this.sortDesc ? -1 : 1 },
|
||||
where: this.where,
|
||||
with: this.populate
|
||||
});
|
||||
// console.log(query)
|
||||
|
||||
if (!this.init) {
|
||||
// this.$router.replace({
|
||||
// query: { query: JSON.stringify(query) }
|
||||
// });
|
||||
return [];
|
||||
}
|
||||
// this.$router.push({
|
||||
// query: { query: JSON.stringify(query) }
|
||||
// });
|
||||
return this.$http
|
||||
.get(this.uri, {
|
||||
params: {
|
||||
query: JSON.stringify(query)
|
||||
computed: {
|
||||
...mapState(["site", "i18n", "auth"]),
|
||||
...mapGetters(["currentLanguage"]),
|
||||
columns() {
|
||||
return Object.entries(this.table.fields).map(([name, field]) => {
|
||||
return {
|
||||
key: name,
|
||||
...field,
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
const { total, data } = res.data;
|
||||
this.total = total;
|
||||
return data;
|
||||
})
|
||||
.catch(e => {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
populate() {
|
||||
return _(this.table.fields || {})
|
||||
.map("ref")
|
||||
.filter()
|
||||
.map(v => v.split(".").shift())
|
||||
.uniq()
|
||||
.toJSON();
|
||||
},
|
||||
actions() {
|
||||
return _.get(this.table, "fields._actions", {});
|
||||
},
|
||||
resource() {
|
||||
return this.$route.params.resource;
|
||||
},
|
||||
uri() {
|
||||
return this.resource.replace(/\./g, "/");
|
||||
}
|
||||
},
|
||||
fetch() {
|
||||
this.init = false;
|
||||
this.$http.get(this.uri + "/grid").then(res => {
|
||||
_.mapValues(res.data.fields, field => {
|
||||
field.thClass = "bg-light";
|
||||
methods: {
|
||||
doSearch(params) {
|
||||
this.where = _.omitBy(params, v => v === null);
|
||||
this.$refs.table.refresh();
|
||||
// console.log(params);
|
||||
},
|
||||
searchAndExport() {
|
||||
const query = JSON.stringify({
|
||||
where: _.clone(this.table.searchModel),
|
||||
with: _.clone(this.populate)
|
||||
});
|
||||
this.$http.get(this.uri + "/export", {
|
||||
responseType: 'arraybuffer',
|
||||
params: {
|
||||
query: query
|
||||
}
|
||||
}).then(res => {
|
||||
const blob = new Blob([res.data]);
|
||||
saveAs(blob, `${this.resource}.xlsx`)
|
||||
})
|
||||
},
|
||||
applyRouteQuery() {
|
||||
const {sort = {}, page = 1, where = {}} = JSON.parse(
|
||||
this.$route.query.query || "{}"
|
||||
);
|
||||
const [sortBy, sortDesc] = Object.entries(sort).pop() || [];
|
||||
sortBy && (this.sortBy = sortBy);
|
||||
|
||||
this.table = res.data;
|
||||
|
||||
if (_.get(this.table, "fields._actions") !== false) {
|
||||
_.set(
|
||||
this.table,
|
||||
"fields._actions.label",
|
||||
this.$t("actions.actions")
|
||||
);
|
||||
if (sortDesc) {
|
||||
this.sortDesc = sortDesc === -1 ? true : false;
|
||||
}
|
||||
this.total = page * this.perPage;
|
||||
this.currentPage = page;
|
||||
this.where = where;
|
||||
this.init = true;
|
||||
if (this.$refs.table) {
|
||||
this.$refs.table.refresh();
|
||||
},
|
||||
remove(id) {
|
||||
if (window.confirm("是否删除?")) {
|
||||
this.$http.delete(`${this.uri}/${id}`).then(res => {
|
||||
this.$snotify.success("删除成功");
|
||||
this.$refs.table.refresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
created() {
|
||||
this.applyRouteQuery();
|
||||
},
|
||||
fetchItems(ctx) {
|
||||
const query = _.merge({}, _.get(this.table, "query"), {
|
||||
page: ctx.currentPage,
|
||||
sort: {[ctx.sortBy]: this.sortDesc ? -1 : 1},
|
||||
where: this.where,
|
||||
with: this.populate
|
||||
});
|
||||
// console.log(query)
|
||||
|
||||
this.fetch();
|
||||
// this.fetchTable();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
if (!this.init) {
|
||||
// this.$router.replace({
|
||||
// query: { query: JSON.stringify(query) }
|
||||
// });
|
||||
return [];
|
||||
}
|
||||
// this.$router.push({
|
||||
// query: { query: JSON.stringify(query) }
|
||||
// });
|
||||
return this.$http
|
||||
.get(this.uri, {
|
||||
params: {
|
||||
query: JSON.stringify(query)
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
const {total, data} = res.data;
|
||||
this.total = total;
|
||||
return data;
|
||||
})
|
||||
.catch(e => {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
fetch() {
|
||||
this.init = false;
|
||||
this.$http.get(this.uri + "/grid").then(res => {
|
||||
_.mapValues(res.data.fields, field => {
|
||||
field.thClass = "bg-light";
|
||||
});
|
||||
|
||||
this.table = res.data;
|
||||
|
||||
if (_.get(this.table, "fields._actions") !== false) {
|
||||
_.set(
|
||||
this.table,
|
||||
"fields._actions.label",
|
||||
this.$t("actions.actions")
|
||||
);
|
||||
}
|
||||
this.init = true;
|
||||
if (this.$refs.table) {
|
||||
this.$refs.table.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
created() {
|
||||
this.applyRouteQuery();
|
||||
|
||||
this.fetch();
|
||||
// this.fetchTable();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3783,6 +3783,11 @@ file-loader@^4.2.0:
|
||||
loader-utils "^1.2.3"
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
file-saver@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a"
|
||||
integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
|
||||
23
poetry.lock
generated
23
poetry.lock
generated
@@ -435,7 +435,7 @@ description = "A SQL query builder API for Python"
|
||||
name = "pypika"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.36.1"
|
||||
version = "0.36.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
@@ -618,8 +618,16 @@ optional = false
|
||||
python-versions = ">=3.6.1"
|
||||
version = "8.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "A Python module for creating Excel XLSX files."
|
||||
name = "xlsxwriter"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.2.8"
|
||||
|
||||
[metadata]
|
||||
content-hash = "ed7e8dd0dbca90c03d8c032fe4fb8f7fe22e83a3c6ec38f087ac335c82225100"
|
||||
content-hash = "725d22fbc5a12792d02318b33d5e05653edb47790ad72078304c987c880cf6b0"
|
||||
python-versions = "^3.8"
|
||||
|
||||
[metadata.files]
|
||||
@@ -822,11 +830,6 @@ markupsafe = [
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
|
||||
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
|
||||
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
|
||||
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||
]
|
||||
orjson = [
|
||||
@@ -882,7 +885,7 @@ pymysql = [
|
||||
{file = "PyMySQL-0.9.2.tar.gz", hash = "sha256:9ec760cbb251c158c19d6c88c17ca00a8632bac713890e465b2be01fdc30713f"},
|
||||
]
|
||||
pypika = [
|
||||
{file = "PyPika-0.36.1.tar.gz", hash = "sha256:bd54953c1cef480cd10d7b602ae6dbb23e0235484914f28c53f476aa2e8edda9"},
|
||||
{file = "PyPika-0.36.3.tar.gz", hash = "sha256:4fd6455955faa9d9016c2e39281a4808f82bbcf6065726f2c73e923a9e32b1bc"},
|
||||
]
|
||||
python-dotenv = [
|
||||
{file = "python-dotenv-0.12.0.tar.gz", hash = "sha256:92b3123fb2d58a284f76cc92bfe4ee6c502c32ded73e8b051c4f6afc8b6751ed"},
|
||||
@@ -1018,3 +1021,7 @@ websockets = [
|
||||
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
|
||||
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
|
||||
]
|
||||
xlsxwriter = [
|
||||
{file = "XlsxWriter-1.2.8-py2.py3-none-any.whl", hash = "sha256:97ab487b81534415c5313154203f3e8a637d792b1e6a8201e8f7f71da0203c2a"},
|
||||
{file = "XlsxWriter-1.2.8.tar.gz", hash = "sha256:488e1988ab16ff3a9cd58c7656d0a58f8abe46ee58b98eecea78c022db28656b"},
|
||||
]
|
||||
|
||||
@@ -16,6 +16,7 @@ aiosqlite = "*"
|
||||
passlib = "*"
|
||||
bcrypt = "*"
|
||||
pyjwt = "*"
|
||||
xlsxwriter = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
taskipy = "*"
|
||||
|
||||
@@ -31,7 +31,7 @@ pycparser==2.20
|
||||
pydantic==1.4
|
||||
pyjwt==1.7.1
|
||||
pymysql==0.9.2
|
||||
pypika==0.36.1
|
||||
pypika==0.36.3
|
||||
python-dotenv==0.12.0
|
||||
python-multipart==0.0.5
|
||||
python-rapidjson==0.9.1
|
||||
@@ -46,3 +46,4 @@ urllib3==1.25.8
|
||||
uvicorn==0.11.3
|
||||
uvloop==0.14.0
|
||||
websockets==8.1
|
||||
xlsxwriter==1.2.8
|
||||
|
||||
Reference in New Issue
Block a user