alessandro trinca tornidor commited on
Commit
8a3e357
·
1 Parent(s): 04a6060

feat: add backend endpoint /thesaurus-wordsapi with related add functions (mongodb persistence)

Browse files
my_ghost_writer/app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import json
2
  from datetime import datetime
3
  from http.client import HTTPException
@@ -10,15 +11,38 @@ from fastapi.exceptions import RequestValidationError
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import FileResponse, JSONResponse
12
  from fastapi.staticfiles import StaticFiles
 
 
13
 
14
- from my_ghost_writer.constants import (ALLOWED_ORIGIN_LIST, API_MODE, DOMAIN, IS_TESTING, LOG_LEVEL, PORT, STATIC_FOLDER,
15
- WORDSAPI_KEY, WORDSAPI_URL, app_logger, RAPIDAPI_HOST)
16
- from my_ghost_writer.thesaurus import get_document_by_word, insert_document
 
17
  from my_ghost_writer.type_hints import RequestTextFrequencyBody, RequestQueryThesaurusWordsapiBody
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  fastapi_title = "My Ghost Writer"
21
- app = FastAPI(title=fastapi_title, version="1.0")
22
  app_logger.info(f"allowed_origins:{ALLOWED_ORIGIN_LIST}, IS_TESTING:{IS_TESTING}, LOG_LEVEL:{LOG_LEVEL}!")
23
  app.add_middleware(
24
  CORSMiddleware,
@@ -26,6 +50,7 @@ app.add_middleware(
26
  allow_credentials=True,
27
  allow_methods=["GET", "POST"]
28
  )
 
29
 
30
 
31
  @app.middleware("http")
@@ -40,10 +65,25 @@ def health():
40
  from nltk import __version__ as nltk_version
41
  from fastapi import __version__ as fastapi_version
42
  from my_ghost_writer.__version__ import __version__ as ghost_writer_version
43
- app_logger.info(f"still alive... FastAPI version:{fastapi_version}, nltk version:{nltk_version}, my-ghost-writer version:{ghost_writer_version}!")
 
44
  return "Still alive..."
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  @app.post("/words-frequency")
48
  def get_words_frequency(body: RequestTextFrequencyBody | str) -> JSONResponse:
49
  from my_ghost_writer.text_parsers import text_stemming
@@ -73,42 +113,49 @@ def get_thesaurus_wordsapi(body: RequestQueryThesaurusWordsapiBody | str) -> JSO
73
  app_logger.info(f"body type: {type(body)} => {body}.")
74
  body_validated = RequestQueryThesaurusWordsapiBody.model_validate_json(body)
75
  query = body_validated.query
76
- try:
77
- response = get_document_by_word(query=query)
78
- t1 = datetime.now()
79
- duration = (t1 - t0).total_seconds()
80
- app_logger.info(f"found local data, duration: {duration:.3f}s.")
81
- return JSONResponse(status_code=200, content={"duration": duration, "thesaurus": response, "source": "local"})
82
- except Exception as e:
83
- app_logger.info(f"e:{e}, document not found?")
84
- url = f"{WORDSAPI_URL}/{query}"
85
- app_logger.info(f"url: {type(url)} => {url}.")
86
- headers = {
87
- "x-rapidapi-key": WORDSAPI_KEY,
88
- "x-rapidapi-host": RAPIDAPI_HOST
89
- }
90
- response = requests.get(url, headers=headers)
91
- t1 = datetime.now()
92
- duration = (t1 - t0).total_seconds()
93
- app_logger.info(f"response.status_code: {response.status_code}, duration: {duration:.3f}s.")
94
- msg = f"API response is not 200: '{response.status_code}', query={query}, url={url}, duration: {duration:.3f}s."
95
  try:
96
- assert response.status_code == 200, msg
97
- response_json = response.json()
98
- insert_document(response_json)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  del response_json["_id"] # since we inserted the wordsapi response on mongodb now it have a bson _id object not serializable by default
100
- t2 = datetime.now()
101
- duration = (t2 - t1).total_seconds()
102
- app_logger.info(f"response_json: inserted json on local db, duration: {duration:.3f}s. ...")
103
- return JSONResponse(status_code=200, content={"duration": duration, "thesaurus": response_json, "source": "wordsapi"})
104
- except AssertionError as ae:
105
- app_logger.error(f"URL: query => {type(query)} {query}; url => {type(url)} {url}.")
106
- app_logger.error(f"headers type: {type(headers)}...")
107
- # app_logger.error(f"headers: {headers}...")
108
- app_logger.error("response:")
109
- app_logger.error(str(response))
110
- app_logger.error(str(ae))
111
- raise HTTPException(ae)
 
112
 
113
 
114
  @app.exception_handler(RequestValidationError)
@@ -128,7 +175,8 @@ def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse
128
  try:
129
  app.mount("/static", StaticFiles(directory=STATIC_FOLDER, html=True), name="static")
130
  except Exception as ex_mount_static:
131
- app_logger.error(f"Failed to mount static folder: {STATIC_FOLDER}, exception: {ex_mount_static}, API_MODE: {API_MODE}!")
 
132
  if not API_MODE:
133
  app_logger.exception(f"since API_MODE is {API_MODE} we will raise the exception!")
134
  raise ex_mount_static
@@ -136,7 +184,6 @@ except Exception as ex_mount_static:
136
  # add the CorrelationIdMiddleware AFTER the @app.middleware("http") decorated function to avoid missing request id
137
  app.add_middleware(CorrelationIdMiddleware)
138
 
139
-
140
  try:
141
  @app.get("/")
142
  @app.get("/static/")
@@ -151,7 +198,8 @@ except Exception as ex_route_main:
151
 
152
  if __name__ == "__main__":
153
  try:
154
- app_logger.info(f"Starting fastapi/gradio application {fastapi_title}, run in api mode: {API_MODE} (no static folder and main route)...")
 
155
  uvicorn.run("my_ghost_writer.app:app", host=DOMAIN, port=PORT, reload=bool(IS_TESTING))
156
  except Exception as ex_run:
157
  print(f"fastapi/gradio application {fastapi_title}, exception:{ex_run}!")
 
1
+ import asyncio
2
  import json
3
  from datetime import datetime
4
  from http.client import HTTPException
 
11
  from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi.responses import FileResponse, JSONResponse
13
  from fastapi.staticfiles import StaticFiles
14
+ from pymongo import __version__ as pymongo_version
15
+ from pymongo.errors import PyMongoError
16
 
17
+ from my_ghost_writer import pymongo_operations_rw
18
+ from my_ghost_writer.constants import (app_logger, ALLOWED_ORIGIN_LIST, API_MODE, DOMAIN, IS_TESTING, LOG_LEVEL, PORT,
19
+ STATIC_FOLDER, WORDSAPI_KEY, WORDSAPI_URL, RAPIDAPI_HOST, MONGO_USE_OK, MONGO_HEALTHCHECK_SLEEP)
20
+ from my_ghost_writer.pymongo_utils import mongodb_health_check
21
  from my_ghost_writer.type_hints import RequestTextFrequencyBody, RequestQueryThesaurusWordsapiBody
22
 
23
 
24
+ async def mongo_health_check_background_task():
25
+ app_logger.info(f"starting task, MONGO_USE_OK:{MONGO_USE_OK}...")
26
+ while MONGO_USE_OK:
27
+ try:
28
+ db_ok["mongo_ok"] = health_mongo() == "Mongodb: still alive..."
29
+ except PyMongoError:
30
+ db_ok["mongo_ok"] = False
31
+ await asyncio.sleep(MONGO_HEALTHCHECK_SLEEP)
32
+
33
+
34
+ async def lifespan(app: FastAPI):
35
+ task = asyncio.create_task(mongo_health_check_background_task())
36
+ yield
37
+ task.cancel()
38
+ try:
39
+ await task
40
+ except asyncio.CancelledError:
41
+ pass
42
+
43
+
44
  fastapi_title = "My Ghost Writer"
45
+ app = FastAPI(title=fastapi_title, version="1.0", lifespan=lifespan)
46
  app_logger.info(f"allowed_origins:{ALLOWED_ORIGIN_LIST}, IS_TESTING:{IS_TESTING}, LOG_LEVEL:{LOG_LEVEL}!")
47
  app.add_middleware(
48
  CORSMiddleware,
 
50
  allow_credentials=True,
51
  allow_methods=["GET", "POST"]
52
  )
53
+ db_ok = {"mongo_ok": MONGO_USE_OK}
54
 
55
 
56
  @app.middleware("http")
 
65
  from nltk import __version__ as nltk_version
66
  from fastapi import __version__ as fastapi_version
67
  from my_ghost_writer.__version__ import __version__ as ghost_writer_version
68
+ app_logger.info(
69
+ f"still alive... FastAPI version:{fastapi_version}, nltk version:{nltk_version}, my-ghost-writer version:{ghost_writer_version}!")
70
  return "Still alive..."
71
 
72
 
73
+ @app.get("/health-mongo")
74
+ def health_mongo() -> str:
75
+ app_logger.info(f"pymongo driver version:{pymongo_version}!")
76
+ if MONGO_USE_OK:
77
+ try:
78
+ db_ok["mongo_ok"] = mongodb_health_check()
79
+ return "Mongodb: still alive..."
80
+ except PyMongoError as pme:
81
+ app_logger.error(str(pme))
82
+ db_ok["mongo_ok"] = False
83
+ raise HTTPException("mongo not ok!")
84
+ return f"MONGO_USE_OK:{MONGO_USE_OK}..."
85
+
86
+
87
  @app.post("/words-frequency")
88
  def get_words_frequency(body: RequestTextFrequencyBody | str) -> JSONResponse:
89
  from my_ghost_writer.text_parsers import text_stemming
 
113
  app_logger.info(f"body type: {type(body)} => {body}.")
114
  body_validated = RequestQueryThesaurusWordsapiBody.model_validate_json(body)
115
  query = body_validated.query
116
+ use_mongo: bool = db_ok["mongo_ok"]
117
+ app_logger.info(f"query: {type(query)} => {query}, use mongo? {use_mongo}.")
118
+ if use_mongo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  try:
120
+ response = pymongo_operations_rw.get_document_by_word(query=query)
121
+ t1 = datetime.now()
122
+ duration = (t1 - t0).total_seconds()
123
+ app_logger.info(f"found local data, duration: {duration:.3f}s.")
124
+ return JSONResponse(status_code=200, content={"duration": duration, "thesaurus": response, "source": "local"})
125
+ except (PyMongoError, AssertionError) as pme:
126
+ app_logger.info(f"{pme}! Let's try the remote service...")
127
+
128
+ url = f"{WORDSAPI_URL}/{query}"
129
+ app_logger.info(f"url: {type(url)} => {url}.")
130
+ headers = {
131
+ "x-rapidapi-key": WORDSAPI_KEY,
132
+ "x-rapidapi-host": RAPIDAPI_HOST
133
+ }
134
+ response = requests.get(url, headers=headers)
135
+ t1 = datetime.now()
136
+ duration = (t1 - t0).total_seconds()
137
+ app_logger.info(f"response.status_code: {response.status_code}, duration: {duration:.3f}s.")
138
+ msg = f"API response is not 200: '{response.status_code}', query={query}, url={url}, duration: {duration:.3f}s."
139
+ try:
140
+ assert response.status_code == 200, msg
141
+ response_json = response.json()
142
+ if use_mongo:
143
+ app_logger.debug(f"use_mongo:{use_mongo}, inserting response '{response_json}' by query '{query}' on db...")
144
+ pymongo_operations_rw.insert_document(response_json)
145
  del response_json["_id"] # since we inserted the wordsapi response on mongodb now it have a bson _id object not serializable by default
146
+ t2 = datetime.now()
147
+ duration = (t2 - t1).total_seconds()
148
+ app_logger.info(f"response_json: inserted json on local db, duration: {duration:.3f}s. ...")
149
+ return JSONResponse(status_code=200,
150
+ content={"duration": duration, "thesaurus": response_json, "source": "wordsapi"})
151
+ except AssertionError as ae:
152
+ app_logger.error(f"URL: query => {type(query)} {query}; url => {type(url)} {url}.")
153
+ app_logger.error(f"headers type: {type(headers)}...")
154
+ # app_logger.error(f"headers: {headers}...")
155
+ app_logger.error("response:")
156
+ app_logger.error(str(response))
157
+ app_logger.error(str(ae))
158
+ raise HTTPException(ae)
159
 
160
 
161
  @app.exception_handler(RequestValidationError)
 
175
  try:
176
  app.mount("/static", StaticFiles(directory=STATIC_FOLDER, html=True), name="static")
177
  except Exception as ex_mount_static:
178
+ app_logger.error(
179
+ f"Failed to mount static folder: {STATIC_FOLDER}, exception: {ex_mount_static}, API_MODE: {API_MODE}!")
180
  if not API_MODE:
181
  app_logger.exception(f"since API_MODE is {API_MODE} we will raise the exception!")
182
  raise ex_mount_static
 
184
  # add the CorrelationIdMiddleware AFTER the @app.middleware("http") decorated function to avoid missing request id
185
  app.add_middleware(CorrelationIdMiddleware)
186
 
 
187
  try:
188
  @app.get("/")
189
  @app.get("/static/")
 
198
 
199
  if __name__ == "__main__":
200
  try:
201
+ app_logger.info(
202
+ f"Starting fastapi/gradio application {fastapi_title}, run in api mode: {API_MODE} (no static folder and main route)...")
203
  uvicorn.run("my_ghost_writer.app:app", host=DOMAIN, port=PORT, reload=bool(IS_TESTING))
204
  except Exception as ex_run:
205
  print(f"fastapi/gradio application {fastapi_title}, exception:{ex_run}!")
my_ghost_writer/constants.py CHANGED
@@ -21,8 +21,17 @@ N_WORDS_GRAM = int(os.getenv("N_WORDS_GRAM", 2))
21
  WORDSAPI_KEY = os.getenv("WORDSAPI_KEY")
22
  WORDSAPI_URL = os.getenv("WORDSAPI_URL", "https://wordsapiv1.p.rapidapi.com/words")
23
  RAPIDAPI_HOST = os.getenv("RAPIDAPI_HOST", "wordsapiv1.p.rapidapi.com")
24
- MONGO_CONNECTION_STRING = os.getenv("MONGO_CONNECTION_STRING", "mongodb://localhost:27017")
25
- MONGO_CONNECTION_TIMEOUT = int(os.getenv("MONGO_CONNECTION_TIMEOUT", 800))
 
 
 
 
 
 
 
 
 
26
  DEFAULT_COLLECTION_THESAURUS =os.getenv("DEFAULT_COLLECTION_THESAURUS", "wordsapi")
27
  session_logger.setup_logging(json_logs=LOG_JSON_FORMAT, log_level=LOG_LEVEL)
28
  app_logger = structlog.stdlib.get_logger(__name__)
 
21
  WORDSAPI_KEY = os.getenv("WORDSAPI_KEY")
22
  WORDSAPI_URL = os.getenv("WORDSAPI_URL", "https://wordsapiv1.p.rapidapi.com/words")
23
  RAPIDAPI_HOST = os.getenv("RAPIDAPI_HOST", "wordsapiv1.p.rapidapi.com")
24
+ MONGO_USE_OK = bool(os.getenv("MONGO_USE_OK", ""))
25
+ MONGO_CONNECTION_STRING_LOCAL = "mongodb://localhost:27017"
26
+ MONGO_CONNECTION_STRING = os.getenv("MONGO_CONNECTION_STRING", MONGO_CONNECTION_STRING_LOCAL)
27
+ MONGO_CONNECTION_TIMEOUT_LOCAL = int(os.getenv("MONGO_CONNECTION_TIMEOUT_LOCAL", 200))
28
+ MONGO_CONNECTION_TIMEOUT_REMOTE = int(os.getenv("MONGO_CONNECTION_TIMEOUT_REMOTE", 3000))
29
+ MONGO_CONNECTION_TIMEOUT = int(os.getenv(
30
+ "MONGO_CONNECTION_TIMEOUT",
31
+ MONGO_CONNECTION_TIMEOUT_LOCAL if MONGO_CONNECTION_STRING == MONGO_CONNECTION_STRING_LOCAL else MONGO_CONNECTION_TIMEOUT_REMOTE
32
+ ))
33
+ MONGO_HEALTHCHECK_SLEEP = int(os.getenv("MONGO_HEALTHCHECK_SLEEP", 900))
34
+ DEFAULT_DBNAME_THESAURUS = "thesaurus"
35
  DEFAULT_COLLECTION_THESAURUS =os.getenv("DEFAULT_COLLECTION_THESAURUS", "wordsapi")
36
  session_logger.setup_logging(json_logs=LOG_JSON_FORMAT, log_level=LOG_LEVEL)
37
  app_logger = structlog.stdlib.get_logger(__name__)
my_ghost_writer/pymongo_get_database.py DELETED
@@ -1,16 +0,0 @@
1
- from pymongo import MongoClient
2
-
3
- from my_ghost_writer.constants import MONGO_CONNECTION_STRING, DEFAULT_COLLECTION_THESAURUS, MONGO_CONNECTION_TIMEOUT
4
-
5
-
6
- def get_database(db_name: str):
7
- # Create a connection using MongoClient. You can import MongoClient or use pymongo.MongoClient
8
- client = MongoClient(MONGO_CONNECTION_STRING, timeoutMS=MONGO_CONNECTION_TIMEOUT)
9
-
10
- # Create the database for our example (we will use the same database throughout the tutorial
11
- return client[db_name]
12
-
13
-
14
- def get_thesaurus_collection(collection_name: str = DEFAULT_COLLECTION_THESAURUS):
15
- dbname = get_database(db_name="thesaurus")
16
- return dbname[collection_name]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
my_ghost_writer/{thesaurus.py → pymongo_operations_rw.py} RENAMED
@@ -1,22 +1,20 @@
1
- from datetime import time
2
  import json
3
- from time import sleep
4
- from bson import json_util
5
  from my_ghost_writer.constants import app_logger
6
- from my_ghost_writer.pymongo_get_database import get_thesaurus_collection
7
 
8
 
9
  def get_document_by_word(query: str) -> dict:
10
- collection = get_thesaurus_collection()
11
  output: dict = collection.find_one({"word": query})
 
12
  del output["_id"]
13
  return output
14
 
15
 
16
  def insert_document(document: dict) -> None:
17
- collection = get_thesaurus_collection()
18
  result = collection.insert_one(document)
19
- print(result)
20
  try:
21
  assert result.inserted_id
22
  except AssertionError:
 
 
1
  import json
2
+ from my_ghost_writer import pymongo_utils
 
3
  from my_ghost_writer.constants import app_logger
 
4
 
5
 
6
  def get_document_by_word(query: str) -> dict:
7
+ collection = pymongo_utils.get_thesaurus_collection()
8
  output: dict = collection.find_one({"word": query})
9
+ assert output, f"not found document with query '{query}'..."
10
  del output["_id"]
11
  return output
12
 
13
 
14
  def insert_document(document: dict) -> None:
15
+ collection = pymongo_utils.get_thesaurus_collection()
16
  result = collection.insert_one(document)
17
+ app_logger.info(f"result:{result}.")
18
  try:
19
  assert result.inserted_id
20
  except AssertionError:
my_ghost_writer/pymongo_utils.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+
3
+ from my_ghost_writer.constants import DEFAULT_DBNAME_THESAURUS, MONGO_CONNECTION_STRING, DEFAULT_COLLECTION_THESAURUS, MONGO_CONNECTION_TIMEOUT, app_logger
4
+
5
+
6
+ def get_client() -> MongoClient:
7
+ client = MongoClient(MONGO_CONNECTION_STRING, timeoutMS=MONGO_CONNECTION_TIMEOUT)
8
+ return client
9
+
10
+
11
+ def get_database(db_name: str = DEFAULT_DBNAME_THESAURUS):
12
+ client = get_client()
13
+ return client[db_name]
14
+
15
+
16
+ def get_thesaurus_collection(db_name: str = DEFAULT_DBNAME_THESAURUS, collection_name: str = DEFAULT_COLLECTION_THESAURUS):
17
+ dbname = get_database(db_name=db_name)
18
+ return dbname[collection_name]
19
+
20
+
21
+ def mongodb_health_check(db_name: str = DEFAULT_DBNAME_THESAURUS, collection_name: str = DEFAULT_COLLECTION_THESAURUS) -> bool:
22
+ client = get_client()
23
+ # Check server is available
24
+ client.admin.command('ping', check=True)
25
+ server_info = client.server_info()
26
+ server_version = server_info["version"]
27
+ app_logger.info(f"mongodb server_version:{server_version}!")
28
+ # Try a simple find operation
29
+ db = client[db_name]
30
+ collection = db[collection_name]
31
+ collection.find_one()
32
+ app_logger.info("mongodb: still alive...")
33
+ return True
my_ghost_writer/type_hints.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from pydantic import BaseModel
2
 
3
 
@@ -13,9 +14,6 @@ class RequestQueryThesaurusWordsapiBody(BaseModel):
13
  query: str
14
 
15
 
16
- from typing import TypedDict
17
-
18
-
19
  class InputTextRow(TypedDict):
20
  """
21
  TypedDict for input text row.
 
1
+ from typing import TypedDict
2
  from pydantic import BaseModel
3
 
4
 
 
14
  query: str
15
 
16
 
 
 
 
17
  class InputTextRow(TypedDict):
18
  """
19
  TypedDict for input text row.
poetry.lock CHANGED
@@ -18,7 +18,7 @@ version = "4.9.0"
18
  description = "High level compatibility layer for multiple asynchronous event loop implementations"
19
  optional = false
20
  python-versions = ">=3.9"
21
- groups = ["webserver"]
22
  files = [
23
  {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
24
  {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
@@ -54,6 +54,18 @@ starlette = ">=0.18"
54
  [package.extras]
55
  celery = ["celery"]
56
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  [[package]]
58
  name = "click"
59
  version = "8.1.8"
@@ -167,7 +179,7 @@ version = "2.7.0"
167
  description = "DNS toolkit"
168
  optional = false
169
  python-versions = ">=3.9"
170
- groups = ["main"]
171
  files = [
172
  {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
173
  {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
@@ -188,7 +200,7 @@ version = "1.2.2"
188
  description = "Backport of PEP 654 (exception groups)"
189
  optional = false
190
  python-versions = ">=3.7"
191
- groups = ["test", "webserver"]
192
  markers = "python_version == \"3.10\""
193
  files = [
194
  {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
@@ -225,19 +237,66 @@ version = "0.14.0"
225
  description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
226
  optional = false
227
  python-versions = ">=3.7"
228
- groups = ["webserver"]
229
  files = [
230
  {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
231
  {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
232
  ]
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  [[package]]
235
  name = "idna"
236
  version = "3.10"
237
  description = "Internationalized Domain Names in Applications (IDNA)"
238
  optional = false
239
  python-versions = ">=3.6"
240
- groups = ["webserver"]
241
  files = [
242
  {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
243
  {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
@@ -464,7 +523,7 @@ version = "4.13.2"
464
  description = "PyMongo - the Official MongoDB Python driver"
465
  optional = false
466
  python-versions = ">=3.9"
467
- groups = ["main"]
468
  files = [
469
  {file = "pymongo-4.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:01065eb1838e3621a30045ab14d1a60ee62e01f65b7cf154e69c5c722ef14d2f"},
470
  {file = "pymongo-4.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ab0325d436075f5f1901cde95afae811141d162bc42d9a5befb647fda585ae6"},
@@ -538,6 +597,21 @@ snappy = ["python-snappy"]
538
  test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
539
  zstd = ["zstandard"]
540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  [[package]]
542
  name = "pytest"
543
  version = "8.3.5"
@@ -705,7 +779,7 @@ version = "1.3.1"
705
  description = "Sniff out which async library your code is running under"
706
  optional = false
707
  python-versions = ">=3.7"
708
- groups = ["webserver"]
709
  files = [
710
  {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
711
  {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -821,12 +895,12 @@ version = "4.13.2"
821
  description = "Backported and Experimental Type Hints for Python 3.8+"
822
  optional = false
823
  python-versions = ">=3.8"
824
- groups = ["main", "webserver"]
825
  files = [
826
  {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
827
  {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
828
  ]
829
- markers = {main = "python_version == \"3.10\""}
830
 
831
  [[package]]
832
  name = "typing-inspection"
@@ -866,4 +940,4 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)
866
  [metadata]
867
  lock-version = "2.1"
868
  python-versions = ">=3.10,<4.0.0"
869
- content-hash = "bc0f1c385864ccdc10f7c7f3d3c886fe231a1dd70b3d45fa342825a49485879c"
 
18
  description = "High level compatibility layer for multiple asynchronous event loop implementations"
19
  optional = false
20
  python-versions = ">=3.9"
21
+ groups = ["main", "test", "webserver"]
22
  files = [
23
  {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
24
  {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
 
54
  [package.extras]
55
  celery = ["celery"]
56
 
57
+ [[package]]
58
+ name = "certifi"
59
+ version = "2025.6.15"
60
+ description = "Python package for providing Mozilla's CA Bundle."
61
+ optional = false
62
+ python-versions = ">=3.7"
63
+ groups = ["main", "test"]
64
+ files = [
65
+ {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"},
66
+ {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"},
67
+ ]
68
+
69
  [[package]]
70
  name = "click"
71
  version = "8.1.8"
 
179
  description = "DNS toolkit"
180
  optional = false
181
  python-versions = ">=3.9"
182
+ groups = ["test", "webserver"]
183
  files = [
184
  {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
185
  {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
 
200
  description = "Backport of PEP 654 (exception groups)"
201
  optional = false
202
  python-versions = ">=3.7"
203
+ groups = ["main", "test", "webserver"]
204
  markers = "python_version == \"3.10\""
205
  files = [
206
  {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
 
237
  description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
238
  optional = false
239
  python-versions = ">=3.7"
240
+ groups = ["main", "test", "webserver"]
241
  files = [
242
  {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
243
  {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
244
  ]
245
 
246
+ [[package]]
247
+ name = "httpcore"
248
+ version = "1.0.8"
249
+ description = "A minimal low-level HTTP client."
250
+ optional = false
251
+ python-versions = ">=3.8"
252
+ groups = ["main", "test"]
253
+ files = [
254
+ {file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"},
255
+ {file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"},
256
+ ]
257
+
258
+ [package.dependencies]
259
+ certifi = "*"
260
+ h11 = ">=0.13,<0.15"
261
+
262
+ [package.extras]
263
+ asyncio = ["anyio (>=4.0,<5.0)"]
264
+ http2 = ["h2 (>=3,<5)"]
265
+ socks = ["socksio (==1.*)"]
266
+ trio = ["trio (>=0.22.0,<1.0)"]
267
+
268
+ [[package]]
269
+ name = "httpx"
270
+ version = "0.28.1"
271
+ description = "The next generation HTTP client."
272
+ optional = false
273
+ python-versions = ">=3.8"
274
+ groups = ["main", "test"]
275
+ files = [
276
+ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
277
+ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
278
+ ]
279
+
280
+ [package.dependencies]
281
+ anyio = "*"
282
+ certifi = "*"
283
+ httpcore = "==1.*"
284
+ idna = "*"
285
+
286
+ [package.extras]
287
+ brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
288
+ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
289
+ http2 = ["h2 (>=3,<5)"]
290
+ socks = ["socksio (==1.*)"]
291
+ zstd = ["zstandard (>=0.18.0)"]
292
+
293
  [[package]]
294
  name = "idna"
295
  version = "3.10"
296
  description = "Internationalized Domain Names in Applications (IDNA)"
297
  optional = false
298
  python-versions = ">=3.6"
299
+ groups = ["main", "test", "webserver"]
300
  files = [
301
  {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
302
  {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
 
523
  description = "PyMongo - the Official MongoDB Python driver"
524
  optional = false
525
  python-versions = ">=3.9"
526
+ groups = ["test", "webserver"]
527
  files = [
528
  {file = "pymongo-4.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:01065eb1838e3621a30045ab14d1a60ee62e01f65b7cf154e69c5c722ef14d2f"},
529
  {file = "pymongo-4.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ab0325d436075f5f1901cde95afae811141d162bc42d9a5befb647fda585ae6"},
 
597
  test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
598
  zstd = ["zstandard"]
599
 
600
+ [[package]]
601
+ name = "pymongo-inmemory"
602
+ version = "0.5.0"
603
+ description = "A mongo mocking library with an ephemeral MongoDB running in memory."
604
+ optional = false
605
+ python-versions = "<4.0,>=3.9"
606
+ groups = ["test"]
607
+ files = [
608
+ {file = "pymongo_inmemory-0.5.0-py3-none-any.whl", hash = "sha256:ebad4ccc9d9bed859ad25932f039aadb476f29c6945df57fdba0f9171f6626a1"},
609
+ {file = "pymongo_inmemory-0.5.0.tar.gz", hash = "sha256:2af2a6bab1cda9a27f524737ce6d3c9ff8cb9e52c224537e5742d610c4aa677e"},
610
+ ]
611
+
612
+ [package.dependencies]
613
+ pymongo = "*"
614
+
615
  [[package]]
616
  name = "pytest"
617
  version = "8.3.5"
 
779
  description = "Sniff out which async library your code is running under"
780
  optional = false
781
  python-versions = ">=3.7"
782
+ groups = ["main", "test", "webserver"]
783
  files = [
784
  {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
785
  {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
 
895
  description = "Backported and Experimental Type Hints for Python 3.8+"
896
  optional = false
897
  python-versions = ">=3.8"
898
+ groups = ["main", "test", "webserver"]
899
  files = [
900
  {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
901
  {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
902
  ]
903
+ markers = {main = "python_version < \"3.13\"", test = "python_version < \"3.13\""}
904
 
905
  [[package]]
906
  name = "typing-inspection"
 
940
  [metadata]
941
  lock-version = "2.1"
942
  python-versions = ">=3.10,<4.0.0"
943
+ content-hash = "a31dd577fbe56afb94b935a7ec1feb8e146f5677cd9edb8869296d272afed9cc"
pyproject.toml CHANGED
@@ -12,23 +12,26 @@ dependencies = [
12
  "nltk (>=3.9.1,<4.0.0)",
13
  "python-dotenv (>=1.1.0,<2.0.0)",
14
  "structlog (>=25.2.0,<26.0.0)",
15
- "pymongo[srv] (>=4.13.2,<5.0.0)",
16
  ]
17
 
18
  [tool.poetry.group.test]
19
  optional = true
20
 
21
  [tool.poetry.group.test.dependencies]
 
22
  pytest = "^8.3.5"
23
  pytest-cov = "^6.1.1"
 
24
 
25
  [tool.poetry.group.webserver]
26
  optional = true
27
 
28
  [tool.poetry.group.webserver.dependencies]
 
29
  fastapi = "^0.115.12"
30
  uvicorn = "^0.34.2"
31
- asgi-correlation-id = "^4.3.4"
32
 
33
  [tool.pytest.ini_options]
34
  addopts = "--cov=my_ghost_writer --cov-report html"
 
12
  "nltk (>=3.9.1,<4.0.0)",
13
  "python-dotenv (>=1.1.0,<2.0.0)",
14
  "structlog (>=25.2.0,<26.0.0)",
15
+ "httpx (>=0.28.1,<0.29.0)"
16
  ]
17
 
18
  [tool.poetry.group.test]
19
  optional = true
20
 
21
  [tool.poetry.group.test.dependencies]
22
+ pymongo-inmemory = "^0.5.0"
23
  pytest = "^8.3.5"
24
  pytest-cov = "^6.1.1"
25
+ httpx = "^0.28.1"
26
 
27
  [tool.poetry.group.webserver]
28
  optional = true
29
 
30
  [tool.poetry.group.webserver.dependencies]
31
+ asgi-correlation-id = "^4.3.4"
32
  fastapi = "^0.115.12"
33
  uvicorn = "^0.34.2"
34
+ pymongo = {extras = ["srv"], version = "^4.13.2"}
35
 
36
  [tool.pytest.ini_options]
37
  addopts = "--cov=my_ghost_writer --cov-report html"
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  nltk==3.9.1
 
2
  python-dotenv==1.1.0
3
- structlog==25.2.0
 
1
  nltk==3.9.1
2
+ pymongo~=4.13.2
3
  python-dotenv==1.1.0
4
+ structlog==25.2.0
template.env CHANGED
@@ -4,6 +4,11 @@ ALLOWED_ORIGIN=http://localhost:7860,http://localhost:8000
4
  WORDSAPI_KEY=
5
  WORDSAPI_URL=https://wordsapiv1.p.rapidapi.com/words
6
  RAPIDAPI_HOST=wordsapiv1.p.rapidapi.com
 
 
7
  MONGO_CONNECTION_STRING=mongodb://localhost:27017
8
- MONGO_CONNECTION_TIMEOUT=800
 
 
 
9
  DEFAULT_COLLECTION_THESAURUS=wordsapi
 
4
  WORDSAPI_KEY=
5
  WORDSAPI_URL=https://wordsapiv1.p.rapidapi.com/words
6
  RAPIDAPI_HOST=wordsapiv1.p.rapidapi.com
7
+ MONGO_USE_OK=
8
+ MONGO_CONNECTION_STRING_LOCAL=mongodb://localhost:27017
9
  MONGO_CONNECTION_STRING=mongodb://localhost:27017
10
+ MONGO_CONNECTION_TIMEOUT_LOCAL=200
11
+ MONGO_CONNECTION_TIMEOUT_REMOTE=3000
12
+ MONGO_CONNECTION_TIMEOUT=
13
+ MONGO_HEALTHCHECK_SLEEP=900
14
  DEFAULT_COLLECTION_THESAURUS=wordsapi