tortoise / tortoise/tortoise-orm

Creating pydantic model from a DB model not working when ReverseRelation is present

Open
#1,238 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.6k
Forks
516
Avg merge
2d 21h
Merged PRs (30d)
9

Description

I am using Tortoise-ORM with FastAPI based application. All python files are in multiple folders under src/backend/, like src/backend/db/, src/backend/schemas/, src/backend/common etc. and my PYTHONPATH env variable is set to src/.

Following are from my DB models module:

from typing import Optional
from datetime import datetime
from pydantic import Json
from tortoise.fields import (
    IntField, DecimalField, CharField, DatetimeField, BooleanField,
    BinaryField, JSONField, ManyToManyField)
from tortoise.fields.relational import (ForeignKeyRelation, ForeignKeyField)
from tortoise.fields import RESTRICT, ReverseRelation, ManyToManyRelation
from tortoise.models import Model


class StatusCheckMixin:
    def is_active(self):
        """
        Function to check if the object is in active state.
        """
        return self.status == "A"

    def is_inactive(self):
        """
        Function to check if the object is in inactive state.
        """
        return self.status == "I"

    def is_deleted(self):
        """
        Function to check if the object is in deleted state.
        """
        return self.status == "D"


class IdentityMixin(Model, StatusCheckMixin):
    """
    Provides numerical identifier and status fields to any model.
    """
    id: int = IntField(
        pk=True, index=True, null=False,
        description="The primary identifier of this entity.")
    status: str = CharField(1, default='A', index=True,
                            description="Status of this entity: A, I, or D.")

    class Meta:
        abstract = True
        ordering = ("-id",)  # Sort in reverse order of value of attribute id


class EntityMixin(Model, StatusCheckMixin):
    """
    Entity (common entity) class for other entities to inherit from. Provides
    numerical identifier, name, and status fields to any model.
    """
    id: int = IntField(
        pk=True, index=True, null=False,
        description="The primary identifier of this entity.")
    name: str = CharField(64, index=True, null=False,
                          description="The name of this entity.")
    status: str = CharField(1, default='A', index=True,
                            description="Status of this entity: A, I, or D.")

    def __str__(self):
        return self.name

    class Meta:
        abstract = True


class CreatorMixin():
    """
    Mixin class to define creator and creation time related attributes for
    Entity type objects.
    """
    created_at: Optional[datetime] = DatetimeField(
        auto_now_add=True, null=False)
    # TODO: Need to pull logged-in user-info to populate it automatically
    # creator_id: Optional[int] = ForeignKeyField(
    #     "models.Authuser", to_field="id", null=True, on_delete=RESTRICT)

    class Meta:
        abstract = True


class LastModifierMixin():
    """
    Mixin class to define modifier and modification time related attributes for
    Entity type objects.
    """
    last_modified_at: Optional[datetime] = DatetimeField(
        null=True, index=True, auto_now=True)
    # TODO: Need to pull logged-in user-info to populate it automatically
    # last_modifier_id: Optional[int] = ForeignKeyField(
    #     "models.Authuser", to_field=id, null=True, on_delete=RESTRICT)
    # last_modifier: Optional["Authuser"] = Relationship()

    class Meta:
        abstract = True


class RemarksMixin():
    """
    Mixin class to define remarks field for an Entity type object.
    """
    remarks: Optional[str] = CharField(
        256, default=None, index=False, null=True)

    class Meta:
        abstract = True


class Authuser(EntityMixin, CreatorMixin, LastModifierMixin):
    """
    Authuser data model.
    """
    login_name: Optional[str] = CharField(
        80, description="Login identifier string.", unique=True, null=False)
    email_address: Optional[str] = CharField(
        80, description="E-mail address of the user.", unique=True, null=False)
    password: Optional[str] = CharField(
        512, description="Password associated with the user.", null=False)
    is_superuser: bool = BooleanField(
        description="Indicates if the user is super-user.", null=False)
    user_realms: ReverseRelation["AuthuserRealm"]

    class Meta:
        table = "authusers"
        table_description = "Stores records of users who can authenticate."
        ordering = ["name", "created_at"]


class Realm(EntityMixin, CreatorMixin, LastModifierMixin, RemarksMixin):
    """
    Class to represent a realm in a multi-tenant application.
    """
    code: Optional[str] = CharField(
        48, description="Realm Code", unique=True, null=False)
    support_pin: Optional[str] = CharField(
        8, index=True, null=True,
        description="PIN to authenticate with support staff.")
    website_url: Optional[str] = CharField(
        80, default=None, description="Website URL", null=True)
    logo_url: Optional[str] = CharField(
        80, default=None, description="Logo URL", null=True)

    class Meta:
        table = 'realms'
        table_description = "Stores records of realms."
        ordering = ["name"]
        unique_together = (("code", "support_pin"), )
        indexes = (("code", "support_pin"), )


class AuthuserRealm(IdentityMixin, RemarksMixin):
    """
    The association class to hold relationship between a user and a realm.
    """
    authuser: ForeignKeyRelation[Authuser] = ForeignKeyField(
        "models.Authuser", related_name="user_realms", on_delete=RESTRICT,
        to_field="id",
        description="The authuser who is mapped to one or more realms.")
    realm: ForeignKeyRelation[Realm] = ForeignKeyField(
        "models.Realm", related_name="user_realms", on_delete=RESTRICT,
        to_field="id",
        description="The associated realm.")
    profile_image_path: Optional[str] = CharField(
        max_length=128, index=True, null=True,
        description="The relative path of profile image.")
    contact_number: Optional[str] = CharField(
        max_length=16, index=True, null=True,
        description="The contact number associated with this mapping.")
    registered_at: Optional[datetime] = DatetimeField(
        auto_now_add=True, description="Timestamp when this mapping was made.")

    async def name(self) -> str:
        return f"{self.authuser.name or ''} ({self.realm.code or ''})".strip()

    class Meta:
        table = 'authuser_realm'
        table_description = "Stores records of authuser-realm mappings."
        ordering = ["id"]
        unique_together = (("authuser_id", "realm_id"), )
        indexes = (("authuser_id", "realm_id", "contact_number", "status"), )

Following are from my schema models module:

from pydantic.main import ModelMetaclass, BaseModel
from tortoise.contrib.pydantic.base import PydanticModel
from typing import Any, Dict, Optional, Tuple
from tortoise.contrib.pydantic import pydantic_model_creator
from datetime import datetime
from tortoise import Tortoise

from backend.db import models as m

Tortoise.init_models(["backend.db.models"], "models")


class AllOptional(ModelMetaclass):
    """
    Fix to enable all optional fields in models used in PUT and PATCH
    HTTP methods.
    """
    def __new__(mcs, name: str, bases: Tuple[type],
                namespaces: Dict[str, Any], **kwargs):
        annotations: dict = namespaces.get('__annotations__', {})
        for base in bases:
            for base_ in base.__mro__:
                if base_ is BaseModel or base_ is PydanticModel:
                    break
                annotations.update(base_.__annotations__)

        for field, value in annotations.items():
            if not field.startswith('__'):
                annotations[field] = Optional[value]
        namespaces['__annotations__'] = annotations
        return super().__new__(mcs, name, bases, namespaces, **kwargs)


AuthuserIn = pydantic_model_creator(
    m.Authuser, name="AuthuserIn", include=(
        "name", "login_name", "email_address", "password", "is_superuser",
        "status"))
AuthuserOut = pydantic_model_creator(
    m.Authuser, name="AuthuserOut", exclude=("password", "user_realms"),
    exclude_readonly=False)
AuthuserMod_ = pydantic_model_creator(
    m.Authuser, name="AuthuserMod_", include=(
        "name", "email_address", "password", "is_superuser", "status"))


class AuthuserMod(AuthuserMod_, metaclass=AllOptional):
    pass


RealmIn = pydantic_model_creator(
    m.Realm, name="RealmIn", include=(
        "name", "code", "status", "logo_url", "website_url", "remarks"))
RealmOut = pydantic_model_creator(
    m.Realm, name="RealmOut", exclude=("support_pin", "user_realms"))
RealmMod_ = pydantic_model_creator(
    m.Realm, name="RealmMod_", exclude=(
        "id", "code", "support_pin", "creator_id", "created_at",
        "last_modifier_id", "last_modified_at", "support_pin"))


class RealmMod(RealmMod_, metaclass=AllOptional):
    pass


class AuthuserRealmOut(BaseModel):
    id: int
    authuser_id: int
    realm_id: int
    registered_at: Optional[datetime]
    profile_image_path: Optional[str]
    contact_number: Optional[str]
    remarks: Optional[str]
    status: Optional[str]

    class Config:
        orm_mode = True


AuthuserRealmMod_ = pydantic_model_creator(
    m.AuthuserRealm, name="AuthuserRealmMod_", include=(
        "profile_image_path", "contact_number", "remarks", "status"))


class AuthuserRealmMod(AuthuserRealmMod_, metaclass=AllOptional):
    pass

I am trying to create an API end point for /realms/ for HTTP POST request in backend/routers/realms.py and following is part of it:

from typing import List
from datetime import datetime, timezone
from fastapi import APIRouter, Query, HTTPException  # , Depends

from ..schemas.models import RealmIn, RealmOut, RealmMod
from ..core.common import get_logger
from ..db import models as m


router = APIRouter(
    prefix='/realms',
    tags=['Realms'],
    responses={404: {"description": "Realm not found."}}
)

@router.post("/", response_model=RealmOut)
async def create_realm(
        realm_in: RealmIn) -> RealmOut:
    """
    Create a new realm from supplied RealmIn object, persist it in DB,
    and then return its Out model along with its id.
    """
    attrs = realm_in.dict()
    realm = await m.Realm.create(**attrs)
    realmout = await RealmOut.from_orm(realm)
    return realmout

I invoked a POST request to this end point with an equivalent command:

curl -X 'POST' \
  'https://my.server:8000/realms/' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Mina Hotels",
  "status": "A",
  "remarks": "Mina Hotels is a Three Star hotel.",
  "code": "mina",
  "website_url": "http://mina.example.in",
  "logo_url": "http://mina.example.in/logo.jpg"
}'

I am getting following exception, when pydantic model's from_orm() method is invoked. I was expecting that the RealmOut model would have been received as response.

...
<snipped>

INFO:     Application startup complete.
Application startup complete.
INFO:     Uvicorn running on http://my.server:8000 (Press CTRL+C to quit)
Uvicorn running on http://my.server:8000 (Press CTRL+C to quit)
INSERT INTO "realms" ("name","status","created_at","last_modified_at","remarks","code","support_pin","website_url","logo_url") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id": ['Mina Hotels', 'A', datetime.datetime(2022, 8, 31, 11, 13, 45, 322861, tzinfo=<UTC>), datetime.datetime(2022, 8, 31, 11, 13, 45, 322997, tzinfo=<UTC>), 'Mina Hotels is a Three Start hotel.', 'mina', None, 'http://mina.example.in', 'http://images.mina.example.in/logo.jpg']
INFO:     172.18.0.2:57232 - "POST /realms/ HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 94, in receive
    return self.receive_nowait()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 89, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 43, in call_next
    message = await recv_stream.receive()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 114, in receive
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 68, in __call__
    response = await self.dispatch_func(request, call_next)
  File "/config/workspace/my_project/src/backend/main.py", line 73, in set_structlog_contextvars
    response = await call_next(request)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 46, in call_next
    raise app_exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 36, in coro
    await self.app(scope, request.receive, send_stream.send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 24, in __call__
    await responder(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 43, in __call__
    await self.app(scope, receive, self.send_with_gzip)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 227, in app
    raw_response = await run_endpoint_function(
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 160, in run_endpoint_function
    return await dependant.call(**values)
  File "/config/workspace/my_project/src/backend/routers/realms.py", line 88, in create_realm
    realmout = await RealmOut.from_orm(realm)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1022, in pydantic.main.validate_model
  File "pydantic/fields.py", line 837, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
  File "pydantic/class_validators.py", line 280, in pydantic.class_validators._generic_validator_cls.lambda3
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/contrib/pydantic/base.py", line 59, in _tortoise_convert
    return list(value)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 104, in __len__
    self._raise_if_not_fetched()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 163, in _raise_if_not_fetched
    raise NoValuesFetched(
tortoise.exceptions.NoValuesFetched: No values were fetched for this relation, first use .fetch_related()
Exception in ASGI application
Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 94, in receive
    return self.receive_nowait()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 89, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 43, in call_next
    message = await recv_stream.receive()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/anyio/streams/memory.py", line 114, in receive
    raise EndOfStream
anyio.EndOfStream

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 68, in __call__
    response = await self.dispatch_func(request, call_next)
  File "/config/workspace/my_project/src/backend/main.py", line 73, in set_structlog_contextvars
    response = await call_next(request)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 46, in call_next
    raise app_exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/base.py", line 36, in coro
    await self.app(scope, request.receive, send_stream.send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 24, in __call__
    await responder(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/gzip.py", line 43, in __call__
    await self.app(scope, receive, self.send_with_gzip)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 92, in __call__
    await self.simple_response(scope, receive, send, request_headers=headers)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/middleware/cors.py", line 147, in simple_response
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 227, in app
    raw_response = await run_endpoint_function(
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/fastapi/routing.py", line 160, in run_endpoint_function
    return await dependant.call(**values)
  File "/config/workspace/my_project/src/backend/routers/realms.py", line 88, in create_realm
    realmout = await RealmOut.from_orm(realm)
  File "pydantic/main.py", line 562, in pydantic.main.BaseModel.from_orm
  File "pydantic/main.py", line 1022, in pydantic.main.validate_model
  File "pydantic/fields.py", line 837, in pydantic.fields.ModelField.validate
  File "pydantic/fields.py", line 1118, in pydantic.fields.ModelField._apply_validators
  File "pydantic/class_validators.py", line 280, in pydantic.class_validators._generic_validator_cls.lambda3
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/contrib/pydantic/base.py", line 59, in _tortoise_convert
    return list(value)
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 104, in __len__
    self._raise_if_not_fetched()
  File "/config/workspace/.virtualenvs/py-3.9/lib/python3.9/site-packages/tortoise/fields/relational.py", line 163, in _raise_if_not_fetched
    raise NoValuesFetched(
tortoise.exceptions.NoValuesFetched: No values were fetched for this relation, first use .fetch_related()

Trying out solutions given for similar errors, like invoking fetch_related() on realm object has not fixed the issue. I am not sure if it is a bug in pydantic model creator code of Tortoise, or I have been doing something incorrectly. I have tried to fix the issues as suggestted below, but the error is still coming up:

https://stackoverflow.com/questions/63754830/python-tortoise-orm-use-related-model-field-in-str

https://github.com/tortoise/tortoise-orm/issues/1069

I am using tortoise-orm 0.19.0 with fastapi 0.78.0.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the schema definitions around schemas/models.py, especially pydantic_model_creator for RealmOut, and trace the call from routers/realms.py through RealmOut.from_orm(realm). Reproduce the POST request with the complete exception, then verify that converting the created Realm with its ReverseRelation produces the expected RealmOut response.

Written by the indexing model from the issue text.

Assessment

Tech stack
fastapi, python
Domain
api, backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.