jowilf / starlette-admin

Fast, beautiful and extensible administrative interface framework for Starlette & FastApi applications
https://jowilf.github.io/starlette-admin/
MIT License
638 stars 59 forks source link

Bug: composite primary key from 2 columns, 1 being a URI, causes exception in 0.13.1 - AssertionError: May not contain path separators #488

Open sglebs opened 10 months ago

sglebs commented 10 months ago

Describe the bug

I have this entity:

class KnowledgeSource(Base):
    __tablename__ = 'knowledge_sources'
    __table_args__ = (PrimaryKeyConstraint('knowledge_ingestion_profile_id', 'uri'),
                      )
    knowledge_ingestion_profile_id = Column(ForeignKey('knowledge_ingestion_profiles.id', ondelete='SET NULL', onupdate='CASCADE'))
    knowledge_ingestion_profile = relationship('KnowledgeIngestionProfile')
    uri = Column(Text)
    status = Column(Enum(KnowledgeSourceState, name='KnowledgeSourceState'), server_default=text("'PENDING'::\"KnowledgeSourceState\""))
    # do we need an ID for it after being incorporated? Or is the URI the ID itself?
    # scenario: The user excludes it, and we need to exclude all the chunks in the vector database
    updated_at = Column(TIMESTAMP(timezone=True, precision=3), nullable=False, server_default=text("CURRENT_TIMESTAMP"))

In 0.11.2 I cannot show it in the admin UI, it gives me an error on startup:

Traceback (most recent call last):
  File "...app/main.py", line 46, in <module>
    create_admin(engine, base_url="/").mount_to(app)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...app/admin/adminviews.py", line 406, in create_admin
    admin.add_view(KnowledgeSourceAdminView(model=KnowledgeSource, icon="fa fa-file"))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette_admin/contrib/sqla/view.py", line 54, in __init__
    assert len(mapper.primary_key) == 1, (
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Multiple PK columns not supported, A possible solution is to override BaseModelView class and put your own logic 

With 0.13.1 I get something different:

  File ".../venv/lib/python3.11/site-packages/starlette/convertors.py", line 26, in to_string
    assert "/" not in value, "May not contain path separators"
           ^^^^^^^^^^^^^^^^

and it comes from here:

 File ".../venv/lib/python3.11/site-packages/starlette_admin/views.py", line 760, in serialize
    request.url_for(route_name + ":detail", identity=self.identity, pk=pk)

This is the source:

    def to_string(self, value: str) -> str:
        value = str(value)
        assert "/" not in value, "May not contain path separators"
        assert value, "Must not be empty"
        return value

NOTE: obviously my entity has a slot/column named "uri" and values ALWAYS do contain "/".

To Reproduce

Have an entity like mine, above. Insert entities via code and try to look at them in the admin. It will cause the error in the console. I suspect you need a string slot and insert something with an "/" in it.

Environment (please complete the following information):

Additional context

I have another regular entity, simple primary key, and it does have uri slot/column. No indues with it. I suspect the problem is related to dual-column PKs. The value of the columns cannot have "/" in the current implementation.

Full stack trace:

Traceback (most recent call last):
  File "...venv/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/fastapi/applications.py", line 284, in __call__
    await super().__call__(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/sessions.py", line 86, in __call__
    await self.app(scope, receive, send_wrapper)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/sessions.py", line 86, in __call__
    await self.app(scope, receive, send_wrapper)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "...venv/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
    raise e
  File "...venv/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
    await self.app(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 443, in handle
    await self.app(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 108, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette_admin/contrib/sqla/middleware.py", line 28, in dispatch
    return await call_next(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 84, in call_next
    raise app_exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 70, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "...venv/lib/python3.11/site-packages/starlette_admin/i18n.py", line 162, in __call__
    await self.app(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/sessions.py", line 86, in __call__
    await self.app(scope, receive, send_wrapper)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 108, in __call__
    response = await self.dispatch_func(request, call_next)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette_admin/auth.py", line 360, in dispatch
    return await call_next(request)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 84, in call_next
    raise app_exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/base.py", line 70, in coro
    await self.app(scope, receive_or_disconnect, send_no_error)
  File "...venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "...venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette_admin/base.py", line 299, in _render_api
    serialized_items = [
                       ^
  File "...venv/lib/python3.11/site-packages/starlette_admin/base.py", line 301, in <listcomp>
    await model.serialize(
  File "...venv/lib/python3.11/site-packages/starlette_admin/views.py", line 760, in serialize
    request.url_for(route_name + ":detail", identity=self.identity, pk=pk)
  File "...venv/lib/python3.11/site-packages/starlette/requests.py", line 178, in url_for
    url_path = router.url_path_for(__name, **path_params)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 643, in url_path_for
    return route.url_path_for(__name, **path_params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 434, in url_path_for
    url = route.url_path_for(remaining_name, **remaining_params)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 259, in url_path_for
    path, remaining_params = replace_params(
                             ^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/routing.py", line 101, in replace_params
    value = convertor.to_string(value)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "...venv/lib/python3.11/site-packages/starlette/convertors.py", line 26, in to_string
    assert "/" not in value, "May not contain path separators"
           ^^^^^^^^^^^^^^^^
AssertionError: May not contain path separators
sglebs commented 9 months ago

My workaround was to add a hash field and use a trigger to compute&set this hash field. I changed the composite primary key to use this hash of the URI instead of the URI itself. This way I avoid "/"/