diff --git a/README.md b/README.md index 0aad393..7c9d22e 100644 --- a/README.md +++ b/README.md @@ -271,8 +271,43 @@ tests/main_test.py ...... [100% - test_update_group : update 테스트 - test_delete_group: delete 테스트 + +### 5) mongoDB 관련 API -### 4) docker compose 실행 +#### (1) `/files` : CSV 파일 업로드 및 JSON insert + +```bash +$ curl -F 'file=@../assets/data/test.csv' -X POST "http://localhost:58000/files/upload" +[{"name":"Alice","age":"20","height":"62","weight":"120.6","_id":"1"},{"name":"Freddie","age":"21","height":"74","weight":"190.6","_id":"2"},{"name":"Bob","age":"17","height":"68","weight":"120.0","_id":"3"}]% + +$ curl -X GET "http://localhost:8000/files/test" +[{"_id":"1","name":"Alice","age":20,"height":62,"weight":120.6},{"_id":"2","name":"Freddie","age":21,"height":74,"weight":190.6},{"_id":"3","name":"Bob","age":17,"height":68,"weight":120.0}]% +``` + +#### (2) `/books` : Book 모델 insert + +```bash +$ curl -X POST "http://localhost:8000/books/" -H "Content-Type: application/json" -d '''{ "_id": "066de609-b04a-4b30-b46c-32537c7f1f6e", + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "..." +} +''' +{"_id":"066de609-b04a-4b30-b46c-32537c7f1f6e","title":"Don Quixote","author":"Miguel de Cervantes","synopsis":"..."}% + +$ curl -X PUT "http://localhost:8000/books/066de609-b04a-4b30-b46c-32537c7f1f6e" -H "Content-Type: application/json" -d '''{ + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "Don Quixote is a Spanish novel by Miguel de Cervantes..." +} +''' +{"_id":"066de609-b04a-4b30-b46c-32537c7f1f6e","title":"Don Quixote","author":"Miguel de Cervantes","synopsis":"Don Quixote is a Spanish novel by Miguel de Cervantes..."}% + +$ curl -X GET "http://localhost:8000/books/" +[{"_id":"066de609-b04a-4b30-b46c-32537c7f1f6e","title":"Don Quixote","author":"Miguel de Cervantes","synopsis":"Don Quixote is a Spanish novel by Miguel de Cervantes..."}]% +``` + +## 4. docker compose 실행 ```bash # 도커 컴포즈에서 linux/amd64 이미지 생성 (Mac M1) @@ -301,7 +336,7 @@ $ docker compose down -v ⠿ Network sqlmodel-pg-api_default Rem... 0.1s ``` -> 참고 +> 참고: git ``` # 신규 리포지토리에 연결시 @@ -316,4 +351,25 @@ git push -u origin main git remote add origin https://github.com/maxmin93/fastapi-sqlmodel-heroes.git git branch -M main git push -u origin main + +# 새로운 브랜치 생성/변경 +git checkout -b db-mongo + +# 새로운 브랜치로 push (최초) +git push --set-upstream origin db-mongo + +# main 브랜치로 변경 +git checkout main + +# 변경사항 로그 조회 +git log --graph --decorate --oneline + +# 변경사항 파일 조회 +git status -u + +# 파일 변경사항 (이전 캐시와 비교) +git diff --cached ../README.md + +# 파일 변경사항 취소 +git restore ../README.md ``` \ No newline at end of file diff --git a/api.env b/api.env index b37745d..b2683a0 100644 --- a/api.env +++ b/api.env @@ -1,2 +1,4 @@ # for api CONN_URL="postgresql://tonyne:tonyne@db:5432/company_db" +MONGO_URI="mongodb://tonyne:tonyne@mongo:27017/" +MONGO_DB="tutorial" \ No newline at end of file diff --git a/assets/data/test.csv b/assets/data/test.csv new file mode 100644 index 0000000..abb3620 --- /dev/null +++ b/assets/data/test.csv @@ -0,0 +1,4 @@ +id,name,age,height,weight +1,Alice,20,62,120.6 +2,Freddie,21,74,190.6 +3,Bob,17,68,120.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index fd08a98..7528549 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ services: db: image: postgres:14 container_name: smp-db + restart: always env_file: - db.env ports: @@ -11,6 +12,19 @@ services: volumes: - smpdb_data:/var/lib/postgresql/data + mongo: + image: mongo:6 + container_name: smp-mongo + restart: always + ports: + - 27017:27017/tcp + environment: + MONGO_INITDB_ROOT_USERNAME: tonyne + MONGO_INITDB_ROOT_PASSWORD: tonyne + volumes: + - smpmg_data:/data/db + - smpmg_cfg:/data/configdb + api: image: py39-slim container_name: smp-api @@ -20,16 +34,21 @@ services: - api.env depends_on: - db + - mongo links: - db + - mongo ports: - 58000:8000 volumes: - smpapi_data:/app - volumes: smpdb_data: name: smpdb_data + smpmg_data: + name: smpmg_data + smpmg_cfg: + name: smpmg_cfg smpapi_data: name: smpapi_data \ No newline at end of file diff --git a/smp-api/app/api/__init__.py b/smp-api/app/api/__init__.py index 41bd4b3..8f9d455 100644 --- a/smp-api/app/api/__init__.py +++ b/smp-api/app/api/__init__.py @@ -1,5 +1,7 @@ -from .heroes import router as hero_router -from .teams import router as team_router -from .tutorials import router as tutorial_router +from api.books import router as book_router +from api.files import router as file_router +from api.heroes import router as hero_router +from api.teams import router as team_router +from api.tutorials import router as tutorial_router -__all__ = [hero_router, team_router, tutorial_router] +__all__ = [hero_router, team_router, tutorial_router, file_router, book_router] diff --git a/smp-api/app/api/books.py b/smp-api/app/api/books.py new file mode 100644 index 0000000..f41e05c --- /dev/null +++ b/smp-api/app/api/books.py @@ -0,0 +1,98 @@ +from typing import List + +from core import get_mongodb +from fastapi import APIRouter, Body, Depends, HTTPException, Response, status +from fastapi.encoders import jsonable_encoder +from loguru import logger +from models import Book, BookUpdate + +# https://pymongo.readthedocs.io/en/stable/tutorial.html +from pymongo.database import Database + +router = APIRouter( + prefix="/books", + tags=["books"], + dependencies=[Depends(get_mongodb)], + responses={404: {"description": "API Not found"}}, +) + + +@router.post( + "/", + response_description="Create a new book", + status_code=status.HTTP_201_CREATED, + response_model=Book, +) +def create_book(book: Book = Body(...), *, db: Database = Depends(get_mongodb)): + """ + curl -X POST "http://localhost:8000/books/" -H "Content-Type: application/json" -d '''{ "_id": "066de609-b04a-4b30-b46c-32537c7f1f6e", + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "..." + } + ''' + """ + book = jsonable_encoder(book) + new_book = db["books"].insert_one(book) + created_book = db["books"].find_one({"_id": new_book.inserted_id}) + logger.info(f"created_book = {created_book}") + return created_book + + +@router.get("/", response_description="List all books", response_model=List[Book]) +def list_books(*, db: Database = Depends(get_mongodb)): + books = list(db["books"].find(limit=100)) + logger.info(f"books.size = {len(books)}") + return books + + +@router.get("/{id}", response_description="Get a single book by id", response_model=Book) +def find_book(id: str, *, db: Database = Depends(get_mongodb)): + if (book := db["books"].find_one({"_id": id})) is not None: + return book + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Book with ID {id} not found" + ) + + +@router.put("/{id}", response_description="Update a book", response_model=Book) +def update_book(id: str, *, db: Database = Depends(get_mongodb), book: BookUpdate = Body(...)): + """ + curl -X PUT "http://localhost:8000/books/066de609-b04a-4b30-b46c-32537c7f1f6e" -H "Content-Type: application/json" -d '''{ + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "Don Quixote is a Spanish novel by Miguel de Cervantes..." + } + ''' + """ + book = {k: v for k, v in book.dict().items() if v is not None} + + if len(book) >= 1: + update_result = db["books"].update_one({"_id": id}, {"$set": book}) + + if update_result.modified_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Book with ID {id} not found" + ) + + # Walrus Operator (since Python 3.8) + if (existing_book := db["books"].find_one({"_id": id})) is not None: + return existing_book + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Book with ID {id} not found" + ) + + +@router.delete("/{id}", response_description="Delete a book") +def delete_book(id: str, *, db: Database = Depends(get_mongodb), response: Response): + delete_result = db["books"].delete_one({"_id": id}) + + if delete_result.deleted_count == 1: + response.status_code = status.HTTP_204_NO_CONTENT + return response + + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Book with ID {id} not found" + ) diff --git a/smp-api/app/api/files.py b/smp-api/app/api/files.py new file mode 100644 index 0000000..4efe36e --- /dev/null +++ b/smp-api/app/api/files.py @@ -0,0 +1,81 @@ +import codecs +import csv +import os +from typing import Any, List + +from core import get_mongodb +from fastapi import APIRouter, Depends, File, HTTPException, UploadFile + +# from fastapi.encoders import jsonable_encoder +from loguru import logger +from models import Person + +# https://pymongo.readthedocs.io/en/stable/tutorial.html +from pymongo.collection import Collection +from pymongo.database import Database + +router = APIRouter( + prefix="/files", + tags=["files"], + dependencies=[Depends(get_mongodb)], + responses={404: {"description": "API Not found"}}, +) + + +@router.get("/hello") +def reader_hello(*, db: Database = Depends(get_mongodb)): + db.drop_collection("hello") + + coll: Collection = db["hello"] + # coll.delete_many({}) + coll.insert_one({"id": 1001, "name": "tonyne", "score": 100.5}) + result = coll.insert_many( + [ + {"id": 1002, "name": "tonyne", "score": 100.5}, + {"id": 1003, "name": "tonyne", "score": 100.5}, + {"id": 1004, "name": "tonyne", "score": 100.5}, + {"id": 1005, "name": "tonyne", "score": 100.5}, + ] + ) + logger.info(f"_ids = {result.inserted_ids}") + cusor = coll.find({}) + for doc in cusor: + logger.info(f"\n{doc}") + return {"msg": "Hello, Files", "collections": db.list_collection_names()} + + +@router.get("/{name}", response_model=List[Person]) +def reader_collection(*, db=Depends(get_mongodb), name: str): + list_of_collections = db.list_collection_names() + if name not in list_of_collections: + raise HTTPException(status_code=404, detail=f"Collection['{name}'] not found") + logger.info(f"Collection['{name}'] found") + collection: Collection = db[name] + return [Person(**r) for r in collection.find({})] + + +def insert_data(collection: Collection, data: List[Any]): + try: + collection.drop() + result = collection.insert_many(data) + except Exception as e: + logger.error(e) + return result.inserted_ids + + +@router.post("/upload") +def reader_upload(file: UploadFile = File(...), *, db: Database = Depends(get_mongodb)): + """ + curl -F 'file=@../assets/data/test.csv' -X POST "http://localhost:8000/files/upload" + """ + csvReader = csv.DictReader(codecs.iterdecode(file.file, "utf-8")) + rows = [] + for row in csvReader: + row["_id"] = str(row.pop("id")) + rows.append(row) + file.file.close() + + collection_name = os.path.splitext(file.filename)[0] + inserted_ids = insert_data(db[collection_name], rows) + logger.info(f"coll['{collection_name}'].ids = {inserted_ids}") + return rows diff --git a/smp-api/app/api/heroes.py b/smp-api/app/api/heroes.py index cfd2e38..7d423df 100644 --- a/smp-api/app/api/heroes.py +++ b/smp-api/app/api/heroes.py @@ -5,7 +5,12 @@ from models import Hero, HeroCreate, HeroRead, HeroReadWithTeam, HeroUpdate from sqlmodel import Session, desc, select -router = APIRouter() +router = APIRouter( + prefix="/pgdb", + tags=["heroes"], + dependencies=[Depends(get_session)], + responses={404: {"description": "API Not found"}}, +) @router.get("/heroes/last", response_model=HeroRead) diff --git a/smp-api/app/api/teams.py b/smp-api/app/api/teams.py index 529f447..3242099 100644 --- a/smp-api/app/api/teams.py +++ b/smp-api/app/api/teams.py @@ -5,7 +5,12 @@ from models import Team, TeamCreate, TeamRead, TeamReadWithHeroes, TeamUpdate from sqlmodel import Session, desc, select -router = APIRouter() +router = APIRouter( + prefix="/pgdb", + tags=["teams"], + dependencies=[Depends(get_session)], + responses={404: {"description": "API Not found"}}, +) # select the last team diff --git a/smp-api/app/api/tutorials.py b/smp-api/app/api/tutorials.py index d19c0ab..82d966d 100644 --- a/smp-api/app/api/tutorials.py +++ b/smp-api/app/api/tutorials.py @@ -4,7 +4,12 @@ from models import Hero, Team from sqlmodel import Session, select -router = APIRouter() +router = APIRouter( + prefix="/pgdb", + tags=["tutorials"], + dependencies=[Depends(get_session)], + responses={404: {"description": "API Not found"}}, +) @router.get("/tutorial/0") diff --git a/smp-api/app/core/__init__.py b/smp-api/app/core/__init__.py index 98da51b..9094385 100644 --- a/smp-api/app/core/__init__.py +++ b/smp-api/app/core/__init__.py @@ -1,4 +1,5 @@ from core.app import app -from core.db import get_session +from core.mgdb import get_mongodb +from core.pgdb import get_session -__all__ = [app, get_session] +__all__ = [app, get_session, get_mongodb] diff --git a/smp-api/app/core/app.py b/smp-api/app/core/app.py index fbde51d..3bdb774 100644 --- a/smp-api/app/core/app.py +++ b/smp-api/app/core/app.py @@ -1,6 +1,7 @@ from fastapi import FastAPI -from core.db import create_db_and_tables, engine +from core.mgdb import get_mongodb +from core.pgdb import create_db_and_tables, engine app = FastAPI(title="FastAPI + SQLModel + PostgreSQL", version="0.1.0") @@ -8,8 +9,12 @@ @app.on_event("startup") def on_startup(): create_db_and_tables() + app.mongodb = get_mongodb() @app.on_event("shutdown") def on_shutdown(): engine.dispose() + app.mongodb.client.close() + app.mongodb = None + # get_mongodb().client.close() diff --git a/smp-api/app/core/mgdb.py b/smp-api/app/core/mgdb.py new file mode 100644 index 0000000..f53615b --- /dev/null +++ b/smp-api/app/core/mgdb.py @@ -0,0 +1,29 @@ +import os + +from dotenv import load_dotenv +from loguru import logger +from pymongo import MongoClient +from pymongo.database import Database +from pymongo.errors import ConnectionFailure + +# MongoDB attributes +ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +ENV_FILE = os.path.join(ROOT_DIR, ".env") +if os.path.exists(ENV_FILE): + load_dotenv(ENV_FILE) + +client = MongoClient(os.environ["MONGO_URI"]) + +# check connection +try: + # The ismaster command is cheap and does not require auth. + client.admin.command("ismaster") + # init database + client.drop_database(os.environ["MONGO_DB"]) +except ConnectionFailure: + logger.error("MongoDB Server not available") + client = None + + +def get_mongodb() -> Database: + return client[os.environ["MONGO_DB"]] diff --git a/smp-api/app/core/db.py b/smp-api/app/core/pgdb.py similarity index 100% rename from smp-api/app/core/db.py rename to smp-api/app/core/pgdb.py diff --git a/smp-api/app/main.py b/smp-api/app/main.py index 2df899e..5565fbe 100644 --- a/smp-api/app/main.py +++ b/smp-api/app/main.py @@ -1,11 +1,13 @@ import uvicorn -from api import hero_router, team_router, tutorial_router +from api import book_router, file_router, hero_router, team_router, tutorial_router from core import app app.include_router(hero_router) app.include_router(team_router) app.include_router(tutorial_router) +app.include_router(file_router) +app.include_router(book_router) @app.get("/") diff --git a/smp-api/app/models/__init__.py b/smp-api/app/models/__init__.py index 95c0718..d72514c 100644 --- a/smp-api/app/models/__init__.py +++ b/smp-api/app/models/__init__.py @@ -1,8 +1,10 @@ -from .hero import HeroCreate, HeroUpdate -from .joined import Hero, HeroRead, HeroReadWithTeam, Team, TeamRead, TeamReadWithHeroes -from .team import TeamCreate, TeamUpdate +from models.book import Book, BookUpdate, Person +from models.hero import HeroCreate, HeroUpdate +from models.joined import Hero, HeroRead, HeroReadWithTeam, Team, TeamRead, TeamReadWithHeroes +from models.team import TeamCreate, TeamUpdate __all__ = [ + # postgres Hero, HeroRead, Team, @@ -13,4 +15,8 @@ TeamUpdate, HeroReadWithTeam, TeamReadWithHeroes, + # mongodb + Person, + Book, + BookUpdate, ] diff --git a/smp-api/app/models/book.py b/smp-api/app/models/book.py new file mode 100644 index 0000000..e655725 --- /dev/null +++ b/smp-api/app/models/book.py @@ -0,0 +1,61 @@ +import uuid +from typing import Optional + +# from bson import ObjectId +from bson.objectid import ObjectId +from pydantic import BaseModel, Field + + +class Person(BaseModel): + id: str = Field(default_factory=str, alias="_id") + name: str + age: Optional[int] = None + height: Optional[int] = None + weight: Optional[float] = None + + class Config: + allow_population_by_field_name = True + arbitrary_types_allowed = True + json_encoders = {ObjectId: str} + schema_extra = { + "example": { + "id": "1", + "name": "Alice", + "age": 20, + "height": 62, + "weight": 120.6, + } + } + + +class Book(BaseModel): + id: str = Field(default_factory=uuid.uuid4, alias="_id") + title: str = Field(...) + author: str = Field(...) + synopsis: str = Field(...) + + class Config: + allow_population_by_field_name = True + schema_extra = { + "example": { + "_id": "066de609-b04a-4b30-b46c-32537c7f1f6e", + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "...", + } + } + + +class BookUpdate(BaseModel): + title: Optional[str] + author: Optional[str] + synopsis: Optional[str] + + class Config: + schema_extra = { + "example": { + "title": "Don Quixote", + "author": "Miguel de Cervantes", + "synopsis": "Don Quixote is a Spanish novel by Miguel de Cervantes...", + } + } diff --git a/smp-api/poetry.lock b/smp-api/poetry.lock index e174cbe..675b191 100644 --- a/smp-api/poetry.lock +++ b/smp-api/poetry.lock @@ -102,7 +102,7 @@ python-versions = "*" name = "certifi" version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -121,7 +121,7 @@ pycparser = "*" name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +category = "dev" optional = false python-versions = ">=3.6.0" @@ -163,6 +163,34 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "dnspython" +version = "2.2.1" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +dnssec = ["cryptography (>=2.6,<37.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + +[[package]] +name = "email-validator" +version = "1.3.0" +description = "A robust email address syntax and deliverability validation library." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + [[package]] name = "entrypoints" version = "0.4" @@ -561,6 +589,7 @@ optional = false python-versions = ">=3.7" [package.dependencies] +email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""} typing-extensions = ">=4.1.0" [package.extras] @@ -578,6 +607,23 @@ python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pymongo" +version = "4.2.0" +description = "Python driver for MongoDB " +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["pymongocrypt (>=1.3.0,<2.0.0)"] +gssapi = ["pykerberos"] +ocsp = ["certifi", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +srv = ["dnspython (>=1.16.0,<3.0.0)"] +zstd = ["zstandard"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -631,6 +677,17 @@ python-versions = ">=3.7" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + [[package]] name = "pywin32" version = "304" @@ -655,7 +712,7 @@ py = {version = "*", markers = "implementation_name == \"pypy\""} name = "requests" version = "2.28.1" description = "Python HTTP for Humans." -category = "main" +category = "dev" optional = false python-versions = ">=3.7, <4" @@ -687,7 +744,7 @@ idna2008 = ["idna"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -825,7 +882,7 @@ python-versions = ">=3.7" name = "urllib3" version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" @@ -883,7 +940,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "ea62979e77b8f991cc977a8ebb7024b3612591eb0e941f974b886bf827028c64" +content-hash = "a1384445a3fcbd83e357dbb0e88fb4f9da92fcfe4dbda3ab1ac069340c55116f" [metadata.files] aiohttp = [ @@ -1109,6 +1166,14 @@ decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +dnspython = [ + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, +] +email-validator = [ + {file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"}, + {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"}, +] entrypoints = [ {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, @@ -1515,6 +1580,75 @@ Pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] +pymongo = [ + {file = "pymongo-4.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b9e4981a65f8500a3a46bb3a1e81b9feb45cf0b2115ad9c4f8d517326d026940"}, + {file = "pymongo-4.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1c81414b706627f15e921e29ae2403aab52e33e36ed92ed989c602888d7c3b90"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:c549bb519456ee230e92f415c5b4d962094caac0fdbcc4ed22b576f66169764e"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:70216ec4c248213ae95ea499b6314c385ce01a5946c448fb22f6c8395806e740"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:8a86e8c2ac2ec87141e1c6cb00bdb18a4560f06e5f96769abcd1dda24dc0e764"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:314b556afd72eb21a6a10bd1f45ef252509f014f80207db59c97372103c88237"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:902e2c9030cb042c49750bc70d72d830d42c64ea0df5ff8630c171e065c93dd7"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c69ef5906dcd6ec565d4d887ba97ceb2a84f3b614307ee3b4780cb1ea40b1867"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07564178ecc203a84f63e72972691af6c0c82d2dc0c9da66ba711695276089ba"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47d5f10922cf7f7dfcd1406bd0926cef6d866a75953c3745502dffd7ac197dd"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cadaaa5c19ad23fc84559e90284f2eb003c36958ebb2c06f286b678f441285f"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d94f535df9f539615bc3dbbef185ded3b609373bb44ca1afffcabac70202678a"}, + {file = "pymongo-4.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:147a23cd96feb67606ac957744d8d25b013426cdc3c7164a4f99bd8253f649e3"}, + {file = "pymongo-4.2.0-cp310-cp310-win32.whl", hash = "sha256:ecdcb0d4e9b08b739035f57a09330efc6f464bd7f942b63897395d996ca6ebd5"}, + {file = "pymongo-4.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c223aea52c359cc8fdee5bd3475532590755c269ec4d4fe581acd47a44e9952"}, + {file = "pymongo-4.2.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:fe0820d169635e41c14a5d21514282e0b93347878666ec9d5d3bf0eed0649948"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e39cacee70a98758f9b2da53ee175378f07c60113b1fa4fae40cbaee5583181e"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:701d331060dae72bf3ebdb82924405d14136a69282ccb00c89fc69dee21340b4"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e08fe1731f5429435b8dea1db9663f9ed1812915ff803fc9991c7c4841ed62ad"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:60c470a58c5b62b1b12a5f5458f8e2f2f67b94e198d03dc5352f854d9230c394"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:b211e161b6cc2790e0d640ad38e0429d06c944e5da23410f4dc61809dba25095"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:ed90a9de4431cbfb2f3b2ef0c5fd356e61c85117b2be4db3eae28cb409f6e2d5"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:68e1e49a5675748233f7b05330f092582cd52f2850b4244939fd75ba640593ed"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:764fc15418d94bce5c2f8ebdbf66544f96f42efb1364b61e715e5b33281b388d"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e64442aba81ed4df1ca494b87bf818569a1280acaa73071c68014f7a884e83f1"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83168126ae2457d1a19b2af665cafa7ef78c2dcff192d7d7b5dad6b36c73ae24"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69d0180bca594e81cdb4a2af328bdb4046f59e10aaeef7619496fe64f2ec918c"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80cbf0b043061451660099fff9001a7faacb2c9c983842b4819526e2f944dc6c"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e1b8f5e2f9637492b0da4d51f78ecb17786e61d6c461ead8542c944750faf4f9"}, + {file = "pymongo-4.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1a957cdc2b26eeed4d8f1889a40c6023dd1bd94672dd0f5ce327314f2caaefd4"}, + {file = "pymongo-4.2.0-cp37-cp37m-win32.whl", hash = "sha256:6bd5888997ea3eae9830c6cc7964b61dcfbc50eb3a5a6ce56ad5f86d5579b11c"}, + {file = "pymongo-4.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:dc24737d24ce0de762bee9c2a884639819485f679bbac8ab5be9c161ef6f9b2c"}, + {file = "pymongo-4.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193cc97d44b1e6d2253ea94e30c6f94f994efb7166e2452af4df55825266e88b"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e152c26ffc30331e9d57591fc4c05453c209aa20ba299d1deb7173f7d1958c22"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a9bc4dcfc2bda69ee88cdb7a89b03f2b8eca668519b704384a264dea2db4209"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8cbb868e88c4eee1c53364bb343d226a3c0e959e791e6828030cb78f46cfcbe3"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:2bfe6b59f431f40fa545547616f4acf0c0c4b64518b1f951083e3bad06eb368b"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:ff66014687598823b6b23751884b4aa67eb934445406d95894dfc60cb7bfcc18"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:31c50da4a080166bc29403aa91f4c76e0889b4f24928d1b60508a37c1bf87f9a"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ccfdc7722df445c49dc6b5d514c3544cad99b53189165f7546793933050ac7fb"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc7ebc37b03956a070260665079665eae69e5e96007694214f3a2107af96816a"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8b4a782aac43948308087b962c9ecb030ba98886ce6dee3ad7aafe8c5e1ce80"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1c23527f8e13f526fededbb96f2e7888f179fe27c51d41c2724f7059b75b2fa"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83cc3c35aeeceb67143914db67f685206e1aa37ea837d872f4bc28d7f80917c9"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e09cdf5aad507c8faa30d97884cc42932ed3a9c2b7f22cc3ccc607bae03981b3"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0f53253f4777cbccc426e669a2af875f26c95bd090d88593287b9a0a8ac7fa25"}, + {file = "pymongo-4.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21238b19243a42f9a34a6d39e7580ceebc6da6d2f3cf729c1cff9023cb61a5f1"}, + {file = "pymongo-4.2.0-cp38-cp38-win32.whl", hash = "sha256:766acb5b1a19eae0f7467bcd3398748f110ea5309cdfc59faa5185dcc7fd4dca"}, + {file = "pymongo-4.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:10f09c4f09757c2e2a707ad7304f5d69cb8fdf7cbfb644dbacfe5bbe8afe311b"}, + {file = "pymongo-4.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6bf01b9237f794fa3bdad5089474067d28be7e199b356a18d3f247a45775f26"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d8bb745321716e7a11220a67c88212ecedde4021e1de4802e563baef9df921d2"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3be53e9888e759c49ae35d747ff77a04ff82b894dd64601e0f3a5a159b406245"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a3efdf154844244e0dabe902cf1827fdced55fa5b144adec2a86e5ce50a99b97"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:a7eb5b06744b911b6668b427c8abc71b6d624e72d3dfffed00988fa1b4340f97"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:b0be613d926c5dbb0d3fc6b58e4f2be4979f80ae76fda6e47309f011b388fe0c"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:e7dcb73f683c155885a3488646fcead3a895765fed16e93c9b80000bc69e96cb"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:b537dd282de1b53d9ae7cf9f3df36420c8618390f2da92100391f3ba8f3c141a"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d98d2a8283c9928a9e5adf2f3c0181e095579e9732e1613aaa55d386e2bcb6c5"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76892bbce743eb9f90360b3626ea92f13d338010a1004b4488e79e555b339921"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:124d0e880b66f9b0778613198e89984984fdd37a3030a9007e5f459a42dfa2d3"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:773467d25c293f8e981b092361dab5fd800e1ba318403b7959d35004c67faedc"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6673ab3fbf3135cc1a8c0f70d480db5b2378c3a70af8d602f73f76b8338bdf97"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:153b8f8705970756226dfeeb7bb9637e0ad54a4d79b480b4c8244e34e16e1662"}, + {file = "pymongo-4.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:01721da74558f2f64a9f162ee063df403ed656b7d84229268d8e4ae99cfba59c"}, + {file = "pymongo-4.2.0-cp39-cp39-win32.whl", hash = "sha256:a25c0eb2d610b20e276e684be61c337396813b636b69373c17314283cb1a3b14"}, + {file = "pymongo-4.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:44b36ccb90aac5ea50be23c1a6e8f24fbfc78afabdef114af16c6e0a80981364"}, + {file = "pymongo-4.2.0.tar.gz", hash = "sha256:72f338f6aabd37d343bd9d1fdd3de921104d395766bcc5cdc4039e4c2dd97766"}, +] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, @@ -1531,6 +1665,9 @@ python-dotenv = [ {file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"}, {file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"}, ] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] pywin32 = [ {file = "pywin32-304-cp310-cp310-win32.whl", hash = "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3"}, {file = "pywin32-304-cp310-cp310-win_amd64.whl", hash = "sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48"}, diff --git a/smp-api/pyproject.toml b/smp-api/pyproject.toml index 5ef67df..9eb748f 100644 --- a/smp-api/pyproject.toml +++ b/smp-api/pyproject.toml @@ -8,14 +8,16 @@ packages = [{include = "smp_api"}] [tool.poetry.dependencies] python = "^3.9" -sqlmodel = "^0.0.8" -fastapi = {extras = ["full"], version = "^0.85.0"} -uvicorn = "^0.18.3" +pytest = "^7.1.3" python-dotenv = "^0.21.0" -psycopg2-binary = "^2.9.3" +pydantic = {extras = ["email"], version = "^1.10.2"} +fastapi = {extras = ["full"], version = "^0.85.0"} +sqlmodel = "^0.0.8" loguru = "^0.6.0" -pytest = "^7.1.3" -requests = "^2.28.1" +psycopg2-binary = "^2.9.3" +pymongo = "^4.2.0" +python-multipart = "^0.0.5" +uvicorn = "^0.18.3" [tool.poetry.group.tests.dependencies] diff --git a/smp-mongo/README.md b/smp-mongo/README.md new file mode 100644 index 0000000..14ced27 --- /dev/null +++ b/smp-mongo/README.md @@ -0,0 +1,128 @@ +# Hero Tutorial of SqlModel + +## 2. Storage: MongoDB + +API 서비스에 업로드 파일을 넣고 데이터를 제공하는 DB: `mongo:6` + +> 참고문서 + +- [hub.docker.com - mongo](https://hub.docker.com/_/mongo) + +### 1) run image with mongo:6 + +```bash +# 바닐라 이미지 실행 +$ docker run -it --rm --name mgdb -p 27017:27017/tcp \ + -e MONGO_INITDB_ROOT_USERNAME=tonyne \ + -e MONGO_INITDB_ROOT_PASSWORD=tonyne \ + mongo:6 + +# DB 서비스만 실행 (포트 매핑이 안될 수 있음) +$ docker compose run -it --rm mongo + +# 정상적인 포트 매핑 상태 +$ docker ps -a +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +d28c5262685f mongo:6 "docker-entrypoint.s…" 5 minutes ago Up 5 minutes 0.0.0.0:27017->27017/tcp smp-mongo +``` + + +### 2) 시행착오 기록 + +#### (1) volume 생성 : data and configdb + +볼륨이 두개 필요하다. (설정 안해도 랜덤 이름으로 2개 생성됨) + +#### (2) 'ObjectId' object is not iterable + +``` +ValueError: [TypeError("'ObjectId' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')] +``` + +csv 파일을 업로드 할 때, FastAPI 에서 ObjectId `_id` 값의 JSON 타입 변환 과정에 오류가 발생 + +- FastAPI 사용시에만 발생하는 문제 +- Pydantic 의 BaseModel 클래스 생성시 ObjectId 처리하는 config 필요 + + 간단한 방법으로, `_id` 값을 강제로 문자열로 변환하여 사용 + +> csv 파일 임포트와 mongoDB 입력 (insert_many) + +```python +@router.post("/upload") +def reader_upload(file: UploadFile = File(...), *, db: Database = Depends(get_mongodb)): + """ + curl -F 'file=@../assets/data/test.csv' -X POST "http://localhost:8000/files/upload" + """ + csvReader = csv.DictReader(codecs.iterdecode(file.file, "utf-8")) + rows = [] + for row in csvReader: + row["_id"] = str(row.pop("id")) + rows.append(row) + file.file.close() + + collection_name = os.path.splitext(file.filename)[0] + inserted_ids = insert_data(db[collection_name], rows) + logger.info(f"coll['{collection_name}'].ids = {inserted_ids}") + return rows +``` + +FastAPI, MongoDB, ObjectID 관련 참고자료 + +- [Handling MongoDB ObjectId in Python-fastAPI](https://medium.com/@madhuri.pednekar/handling-mongodb-objectid-in-python-fastapi-4dd1c7ad67cd) + + JSONEncoder 사용법 +- [ISSUE: FastApi & MongoDB - the full guide #1515](https://github.com/tiangolo/fastapi/issues/1515#issuecomment-782838556) + + ObjectId 의 wrapper class 생성 (타입 처리) + + +### 3) mongodb insert_test + +#### (1) 간단한 insert_test + +일반적인 mongoDB 사용시에는 문제가 안된다. (아리송) + +```python +from pymongo.collection import Collection +from pymongo.database import Database + +def insert_test(db: Database): + db.drop_collection("hello") + + coll: Collection = db["hello"] + # coll.delete_many({}) + coll.insert_one({"id": 1001, "name": "tonyne", "score": 100.5}) + result = coll.insert_many( + [ + {"id": 1002, "name": "tonyne", "score": 100.5}, + {"id": 1003, "name": "tonyne", "score": 100.5}, + {"id": 1004, "name": "tonyne", "score": 100.5}, + {"id": 1005, "name": "tonyne", "score": 100.5}, + ] + ) + logger.info(f"_ids = {result.inserted_ids}") + cusor = coll.find({}) + for doc in cusor: + logger.info(f"\n{doc}") + return {"msg": "Hello, Files", "collections": db.list_collection_names()} + +""" +api.files:insert_test:45 - _ids = [ObjectId('6340eb861c2cbd9dbb04fe94'), ObjectId('6340eb861c2cbd9dbb04fe95'), ObjectId('6340eb861c2cbd9dbb04fe96'), ObjectId('6340eb861c2cbd9dbb04fe97')] +api.files:insert_test:48 - +{'_id': ObjectId('6340eb861c2cbd9dbb04fe93'), 'id': 1001, 'name': 'tonyne', 'score': 100.5} +api.files:insert_test:48 - +{'_id': ObjectId('6340eb861c2cbd9dbb04fe94'), 'id': 1002, 'name': 'tonyne', 'score': 100.5} +api.files:insert_test:48 - +{'_id': ObjectId('6340eb861c2cbd9dbb04fe95'), 'id': 1003, 'name': 'tonyne', 'score': 100.5} +api.files:insert_test:48 - +{'_id': ObjectId('6340eb861c2cbd9dbb04fe96'), 'id': 1004, 'name': 'tonyne', 'score': 100.5} +api.files:insert_test:48 - +{'_id': ObjectId('6340eb861c2cbd9dbb04fe97'), 'id': 1005, 'name': 'tonyne', 'score': 100.5} +""" +``` + +#### (2) insert_test 실행시 mongodb 로그 + +```bash +smp-mongo | {"t":{"$date":"2022-10-08T03:46:11.600+00:00"},"s":"I", "c":"COMMAND", "id":518070, "ctx":"conn5","msg":"CMD: drop","attr":{"namespace":"tutorial.hello"}} +smp-mongo | {"t":{"$date":"2022-10-08T03:46:11.604+00:00"},"s":"I", "c":"STORAGE", "id":20320, "ctx":"conn5","msg":"createCollection","attr":{"namespace":"tutorial.hello","uuidDisposition":"generated","uuid":{"uuid":{"$uuid":"c7bb4a7f-314c-4751-80ab-463cb6105f9c"}},"options":{}}} +smp-mongo | {"t":{"$date":"2022-10-08T03:46:11.610+00:00"},"s":"I", "c":"INDEX", "id":20345, "ctx":"conn5","msg":"Index build: done building","attr":{"buildUUID":null,"collectionUUID":{"uuid":{"$uuid":"c7bb4a7f-314c-4751-80ab-463cb6105f9c"}},"namespace":"tutorial.hello","index":"_id_","ident":"index-1--6041161298439540684","collectionIdent":"collection-0--6041161298439540684","commitTimestamp":null}} +``` \ No newline at end of file