Spaces:
Sleeping
Sleeping
add new files for docker and fast api
Browse files- .vscode/settings.json +16 -0
- Dockerfile +38 -0
- LICENSE +21 -0
- app.py +9 -21
- docker-compose.yml +29 -0
- main.py +42 -34
- run.py +9 -0
- static/css/style.css +64 -0
- templates/error.html +11 -0
- templates/home.html +11 -0
- tox.ini +5 -0
.vscode/settings.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"python.linting.pylint" : false,
|
| 3 |
+
"python.linting.flake8" : true,
|
| 4 |
+
"python.linting.enabled" : true,
|
| 5 |
+
"python.formatting.black" : true,
|
| 6 |
+
"editor.formatOnSave": true,
|
| 7 |
+
"python.linting.flake8Args":[
|
| 8 |
+
"--max-line-length=88"
|
| 9 |
+
],
|
| 10 |
+
"python.testing.unittestEnabled": false,
|
| 11 |
+
"python.testing.pytestEnabled": true,
|
| 12 |
+
"python.testing.autoTestDiscoverOnSaveEnabled": true,
|
| 13 |
+
"python.testing.pytestArgs": [
|
| 14 |
+
"tests"
|
| 15 |
+
],
|
| 16 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim as builder
|
| 2 |
+
|
| 3 |
+
# set environment variables
|
| 4 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 5 |
+
PYTHONFAULTHANDLER=1 \
|
| 6 |
+
PYTHONUNBUFFERED=1
|
| 7 |
+
ENV POETRY_NO_INTERACTION=1 \
|
| 8 |
+
POETRY_VIRTUALENVS_IN_PROJECT=1 \
|
| 9 |
+
POETRY_VIRTUALENVS_CREATE=1 \
|
| 10 |
+
POETRY_CACHE_DIR=/tmp/poetry_cache
|
| 11 |
+
|
| 12 |
+
RUN mkdir -p /app
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
RUN pip install poetry
|
| 15 |
+
COPY poetry.lock pyproject.toml ./
|
| 16 |
+
RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --no-root
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
FROM python:3.10-slim as base
|
| 20 |
+
|
| 21 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 22 |
+
PYTHONFAULTHANDLER=1 \
|
| 23 |
+
PYTHONUNBUFFERED=1
|
| 24 |
+
|
| 25 |
+
RUN apt-get update && apt-get install -y curl
|
| 26 |
+
WORKDIR /app
|
| 27 |
+
ENV VIRTUAL_ENV=/app/.venv \
|
| 28 |
+
PATH="/app/.venv/bin:$PATH"
|
| 29 |
+
|
| 30 |
+
COPY --from=builder /app/.venv /app/.venv
|
| 31 |
+
|
| 32 |
+
# Necessary Files
|
| 33 |
+
COPY main.py app.py /app/
|
| 34 |
+
|
| 35 |
+
# Expose port
|
| 36 |
+
EXPOSE 8000
|
| 37 |
+
|
| 38 |
+
CMD ["uvicorn","main:app","--proxy-headers","--host","0.0.0.0","--port","8000","--workers","3"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023 Alexander Frantsev
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
app.py
CHANGED
|
@@ -75,10 +75,10 @@ class Summarizer():
|
|
| 75 |
self.ru_sentiment_pipe = pipeline("sentiment-analysis", model=RU_SENTIMENT_MODEL)
|
| 76 |
self.en_summary_pipe = sum_pipe
|
| 77 |
self.en_sentiment_pipe = pipeline("sentiment-analysis", model=EN_SENTIMENT_MODEL)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def mT5_summarize(self, text: str) -> str:
|
| 81 |
|
|
|
|
|
|
|
|
|
|
| 82 |
WHITESPACE_HANDLER = lambda k: re.sub('\s+', ' ', re.sub('\n+', ' ', k.strip()))
|
| 83 |
|
| 84 |
input_ids = self.sum_tokenizer(
|
|
@@ -111,23 +111,8 @@ class Summarizer():
|
|
| 111 |
sentiment = {'en': self.en_sentiment_pipe,
|
| 112 |
'ru': self.ru_sentiment_pipe,}
|
| 113 |
return summary[lang], sentiment[lang]
|
| 114 |
-
|
| 115 |
-
# def summarize(self, text: str, lang: str = 'en') -> str:
|
| 116 |
-
# result = {}
|
| 117 |
-
# sum_pipe, sent_pipe = self.get_pipe(lang)
|
| 118 |
-
|
| 119 |
-
# response_summary = sum_pipe(text)
|
| 120 |
-
# logger.info(response_summary)
|
| 121 |
-
# result["summary"] = response_summary[0]["summary_text"]
|
| 122 |
-
|
| 123 |
-
# response_sentiment = sent_pipe(text)
|
| 124 |
-
# logger.info(response_sentiment)
|
| 125 |
-
# result["sentiment_label"] = response_sentiment[0]["label"]
|
| 126 |
-
# result["sentiment_score"] = response_sentiment[0]["score"]
|
| 127 |
-
|
| 128 |
-
# return f"Summary: {result['summary']}\n Sentiment: {result['sentiment_label']} ({result['sentiment_score']:.3f})"
|
| 129 |
|
| 130 |
-
def summarize(self, text: Request, lang: str = 'en') ->
|
| 131 |
sum_pipe, sent_pipe = self.get_pipe(lang)
|
| 132 |
response_summary = sum_pipe(text)
|
| 133 |
logger.info(response_summary)
|
|
@@ -139,6 +124,9 @@ class Summarizer():
|
|
| 139 |
sentiment_score=response_sentiment[0]["score"],
|
| 140 |
)
|
| 141 |
return result
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
if __name__ == "__main__":
|
| 144 |
pipe = Summarizer()
|
|
@@ -161,12 +149,12 @@ if __name__ == "__main__":
|
|
| 161 |
ru_inbtn = gr.Button("Запустить")
|
| 162 |
|
| 163 |
en_inbtn.click(
|
| 164 |
-
pipe.
|
| 165 |
[en_inputs, en_lang],
|
| 166 |
[en_outputs],
|
| 167 |
)
|
| 168 |
ru_inbtn.click(
|
| 169 |
-
pipe.
|
| 170 |
[ru_inputs, ru_lang],
|
| 171 |
[ru_outputs],
|
| 172 |
)
|
|
|
|
| 75 |
self.ru_sentiment_pipe = pipeline("sentiment-analysis", model=RU_SENTIMENT_MODEL)
|
| 76 |
self.en_summary_pipe = sum_pipe
|
| 77 |
self.en_sentiment_pipe = pipeline("sentiment-analysis", model=EN_SENTIMENT_MODEL)
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
def mT5_summarize(self, text: str) -> str:
|
| 80 |
+
'''Handle text with mT5 model without pipeline'''
|
| 81 |
+
|
| 82 |
WHITESPACE_HANDLER = lambda k: re.sub('\s+', ' ', re.sub('\n+', ' ', k.strip()))
|
| 83 |
|
| 84 |
input_ids = self.sum_tokenizer(
|
|
|
|
| 111 |
sentiment = {'en': self.en_sentiment_pipe,
|
| 112 |
'ru': self.ru_sentiment_pipe,}
|
| 113 |
return summary[lang], sentiment[lang]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
+
def summarize(self, text: Request, lang: str = 'en') -> Result:
|
| 116 |
sum_pipe, sent_pipe = self.get_pipe(lang)
|
| 117 |
response_summary = sum_pipe(text)
|
| 118 |
logger.info(response_summary)
|
|
|
|
| 124 |
sentiment_score=response_sentiment[0]["score"],
|
| 125 |
)
|
| 126 |
return result
|
| 127 |
+
|
| 128 |
+
def summ(self, text: Request, lang: str = 'en') -> str:
|
| 129 |
+
return self.summarize(text, lang).to_str()
|
| 130 |
|
| 131 |
if __name__ == "__main__":
|
| 132 |
pipe = Summarizer()
|
|
|
|
| 149 |
ru_inbtn = gr.Button("Запустить")
|
| 150 |
|
| 151 |
en_inbtn.click(
|
| 152 |
+
pipe.summ,
|
| 153 |
[en_inputs, en_lang],
|
| 154 |
[en_outputs],
|
| 155 |
)
|
| 156 |
ru_inbtn.click(
|
| 157 |
+
pipe.summ,
|
| 158 |
[ru_inputs, ru_lang],
|
| 159 |
[ru_outputs],
|
| 160 |
)
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
services:
|
| 3 |
+
app:
|
| 4 |
+
container_name: summary-app
|
| 5 |
+
restart: unless-stopped
|
| 6 |
+
# env_file:
|
| 7 |
+
# - .env
|
| 8 |
+
# environment:
|
| 9 |
+
# PGHOST: 'db'
|
| 10 |
+
# PGDATABASE: ${DATABASE_NAME}
|
| 11 |
+
# PGUSER: ${DATABASE_USER}
|
| 12 |
+
# PGPASSWORD: ${DATABASE_PASS}
|
| 13 |
+
build:
|
| 14 |
+
context: .
|
| 15 |
+
dockerfile: Dockerfile
|
| 16 |
+
ports:
|
| 17 |
+
- "8000:8000"
|
| 18 |
+
expose:
|
| 19 |
+
- 8000
|
| 20 |
+
healthcheck:
|
| 21 |
+
test: curl --fail -s http://localhost:8000/ || exit 1
|
| 22 |
+
interval: 10s
|
| 23 |
+
timeout: 5s
|
| 24 |
+
retries: 3
|
| 25 |
+
start_period: 10s
|
| 26 |
+
command: ["uvicorn","main:app","--proxy-headers","--host","0.0.0.0","--port","8000","--workers","3"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
main.py
CHANGED
|
@@ -8,40 +8,48 @@ from app import DEFAULT_EN_TEXT, DEFAULT_RU_TEXT
|
|
| 8 |
app = FastAPI()
|
| 9 |
pipe = Summarizer()
|
| 10 |
|
| 11 |
-
|
| 12 |
@app.post("/summ_ru", response_model=Result)
|
| 13 |
async def ru_summ_api(request: Request):
|
| 14 |
-
results = pipe.summarize(request.text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
return results
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
| 8 |
app = FastAPI()
|
| 9 |
pipe = Summarizer()
|
| 10 |
|
|
|
|
| 11 |
@app.post("/summ_ru", response_model=Result)
|
| 12 |
async def ru_summ_api(request: Request):
|
| 13 |
+
results = pipe.summarize(request.text, lang='ru')
|
| 14 |
+
return results
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@app.post("/summ_en", response_model=Result)
|
| 19 |
+
async def ru_summ_api(request: Request):
|
| 20 |
+
results = pipe.summarize(request.text, lang='en')
|
| 21 |
return results
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
with gr.Blocks() as demo:
|
| 25 |
+
with gr.Row():
|
| 26 |
+
with gr.Column(scale=2, min_width=600):
|
| 27 |
+
en_sum_description=gr.Markdown(value=f"Model for Summary: {EN_SUMMARY_MODEL}")
|
| 28 |
+
en_sent_description=gr.Markdown(value=f"Model for Sentiment: {EN_SENTIMENT_MODEL}")
|
| 29 |
+
en_inputs=gr.Textbox(label="en_input", lines=5, value=DEFAULT_EN_TEXT, placeholder=DEFAULT_EN_TEXT)
|
| 30 |
+
en_lang=gr.Textbox(value='en',visible=False)
|
| 31 |
+
en_outputs=gr.Textbox(label="en_output", lines=5, placeholder="Summary and Sentiment would be here...")
|
| 32 |
+
en_inbtn = gr.Button("Proceed")
|
| 33 |
+
with gr.Column(scale=2, min_width=600):
|
| 34 |
+
ru_sum_description=gr.Markdown(value=f"Model for Summary: {RU_SUMMARY_MODEL}")
|
| 35 |
+
ru_sent_description=gr.Markdown(value=f"Model for Sentiment: {RU_SENTIMENT_MODEL}")
|
| 36 |
+
ru_inputs=gr.Textbox(label="ru_input", lines=5, value=DEFAULT_RU_TEXT, placeholder=DEFAULT_RU_TEXT)
|
| 37 |
+
ru_lang=gr.Textbox(value='ru',visible=False)
|
| 38 |
+
ru_outputs=gr.Textbox(label="ru_output", lines=5, placeholder="Здесь будет обобщение и эмоциональный окрас текста...")
|
| 39 |
+
ru_inbtn = gr.Button("Запустить")
|
| 40 |
+
|
| 41 |
+
en_inbtn.click(
|
| 42 |
+
pipe.summ,
|
| 43 |
+
[en_inputs, en_lang],
|
| 44 |
+
[en_outputs],
|
| 45 |
+
)
|
| 46 |
+
ru_inbtn.click(
|
| 47 |
+
pipe.summ,
|
| 48 |
+
[ru_inputs, ru_lang],
|
| 49 |
+
[ru_outputs],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# demo.launch(show_api=False)
|
| 53 |
+
|
| 54 |
+
# mounting at the root path
|
| 55 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
run.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
uvicorn.run(
|
| 5 |
+
app="main:app",
|
| 6 |
+
host="localhost",
|
| 7 |
+
port=8000,
|
| 8 |
+
reload=True
|
| 9 |
+
)
|
static/css/style.css
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
display: flex;
|
| 3 |
+
justify-content: center;
|
| 4 |
+
align-items: center;
|
| 5 |
+
height: 100vh;
|
| 6 |
+
margin: 0;
|
| 7 |
+
background-color: #f0f0f0;
|
| 8 |
+
font-family: Arial, sans-serif;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.container {
|
| 12 |
+
height: 50vh;
|
| 13 |
+
width: 25vw;
|
| 14 |
+
display: flex;
|
| 15 |
+
padding-block: 2rem;
|
| 16 |
+
justify-content: space-between;
|
| 17 |
+
flex-direction: column;
|
| 18 |
+
align-items: center;
|
| 19 |
+
border: 1px solid #47616c;
|
| 20 |
+
border-radius: 12px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.header {
|
| 24 |
+
display: flex;
|
| 25 |
+
flex-direction: column;
|
| 26 |
+
align-items: center;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.img-box {
|
| 30 |
+
width: 70px;
|
| 31 |
+
height: 70px;
|
| 32 |
+
border-radius: 50%;
|
| 33 |
+
overflow: hidden;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.img {
|
| 37 |
+
width: 100%;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.google-btn {
|
| 41 |
+
display: flex;
|
| 42 |
+
align-items: center;
|
| 43 |
+
background-color: #47616c;
|
| 44 |
+
color: white;
|
| 45 |
+
width: 200px;
|
| 46 |
+
height: 50px;
|
| 47 |
+
border-radius: 5px;
|
| 48 |
+
box-shadow: 0px 3px 10px -2px rgba(0, 0, 0, 0.15);
|
| 49 |
+
overflow: hidden;
|
| 50 |
+
padding-inline: 8px;
|
| 51 |
+
text-decoration: none;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.google-icon {
|
| 55 |
+
background: url("https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg") transparent 5px 50% no-repeat;
|
| 56 |
+
display: inline-block;
|
| 57 |
+
vertical-align: middle;
|
| 58 |
+
width: 35px;
|
| 59 |
+
height: 50px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.btn-text {
|
| 63 |
+
margin-left: 10px;
|
| 64 |
+
}
|
templates/error.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Error</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<h2>{{error}}</h2>
|
| 10 |
+
</body>
|
| 11 |
+
</html>
|
templates/home.html
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8"/>
|
| 5 |
+
<link rel="stylesheet" href="{{ url_for('static',path='/css/style.css')}}"/>
|
| 6 |
+
<title>Home Page</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<h1>Welcome!</h1>
|
| 10 |
+
</body>
|
| 11 |
+
</html>
|
tox.ini
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[flake8]
|
| 2 |
+
exclude = .git, __pycache__, env, .venv
|
| 3 |
+
max-line-length = 88
|
| 4 |
+
max-complexity = 18
|
| 5 |
+
extend-ignore = E203, E266, E501, W503
|