Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/api/jobs/search not robust enough with DateTime? #341

Open
aldbr opened this issue Dec 5, 2024 · 4 comments
Open

/api/jobs/search not robust enough with DateTime? #341

aldbr opened this issue Dec 5, 2024 · 4 comments
Assignees

Comments

@aldbr
Copy link
Contributor

aldbr commented Dec 5, 2024

Looks like diracX is not able to process submission time format coming from diracX-web:

{
  "detail": "Cannot parse value='2024-12-05T09:21:57.026Z'"
}

Not sure yet whether it's the fault of diracX or diracX-web, but it sounds like diracX is not robust enough:

"search": [
        {
            "parameter": "SubmissionTime",
            "operator": "gt",
            "value": "2024-12-05T09:21:57"
        }
    ]
2024-12-05 10:41:27,797 - INFO:     2001:1458:204:1::101:b371:0 - "POST /api/jobs/search?page=1&per_page=25 HTTP/1.1" 500 Internal Server Error
2024-12-05 10:41:27,798 - ERROR:    Exception in ASGI application
  + Exception Group Traceback (most recent call last):
  |   File "/opt/conda/lib/python3.11/site-packages/starlette/_utils.py", line 76, in collapse_excgroups
  |     yield
  |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/base.py", line 186, in __call__
  |     async with anyio.create_task_group() as task_group:
  |   File "/opt/conda/lib/python3.11/site-packages/anyio/_backends/_asyncio.py", line 763, in __aexit__
  |     raise BaseExceptionGroup(
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/opt/conda/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409, in run_asgi
    |     result = await app(  # type: ignore[func-returns-value]
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
    |     return await self.app(scope, receive, send)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__
    |     await super().__call__(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/applications.py", line 113, in __call__
    |     await self.middleware_stack(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/errors.py", line 187, in __call__
    |     raise exc
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/errors.py", line 165, in __call__
    |     await self.app(scope, receive, _send)
    |   File "/opt/conda/lib/python3.11/site-packages/opentelemetry/instrumentation/asgi/__init__.py", line 579, in __call__
    |     await self.app(scope, otel_receive, otel_send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/cors.py", line 93, in __call__
    |     await self.simple_response(scope, receive, send, request_headers=headers)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/cors.py", line 144, in simple_response
    |     await self.app(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/base.py", line 185, in __call__
    |     with collapse_excgroups():
    |   File "/opt/conda/lib/python3.11/contextlib.py", line 158, in __exit__
    |     self.gen.throw(typ, value, traceback)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/_utils.py", line 82, in collapse_excgroups
    |     raise exc
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/base.py", line 187, in __call__
    |     response = await self.dispatch_func(request, call_next)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/diracx/routers/__init__.py", line 491, in dispatch
    |     response = await call_next(request)
    |                ^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/base.py", line 163, in call_next
    |     raise app_exc
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/base.py", line 149, in coro
    |     await self.app(scope, receive_or_disconnect, send_no_error)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    |     await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    |     raise exc
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    |     await app(scope, receive, sender)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/routing.py", line 715, in __call__
    |     await self.middleware_stack(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/routing.py", line 735, in app
    |     await route.handle(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/routing.py", line 288, in handle
    |     await self.app(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/routing.py", line 76, in app
    |     await wrap_app_handling_exceptions(app, request)(scope, receive, send)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    |     raise exc
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    |     await app(scope, receive, sender)
    |   File "/opt/conda/lib/python3.11/site-packages/starlette/routing.py", line 73, in app
    |     response = await f(request)
    |                ^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/fastapi/routing.py", line 301, in app
    |     raw_response = await run_endpoint_function(
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
    |     return await dependant.call(**values)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/diracx/routers/job_manager/__init__.py", line 608, in search
    |     total, jobs = await job_db.search(
    |                   ^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/diracx/db/sql/job/db.py", line 95, in search
    |     total = (await self.conn.execute(total_count_stmt)).scalar_one()
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/engine.py", line 657, in execute
    |     result = await greenlet_spawn(
    |              ^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 201, in greenlet_spawn
    |     result = context.throw(*sys.exc_info())
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1418, in execute
    |     return meth(
    |            ^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/sql/elements.py", line 515, in _execute_on_connection
    |     return connection._execute_clauseelement(
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1640, in _execute_clauseelement
    |     ret = self._execute_context(
    |           ^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
    |     return self._exec_single_context(
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
    |     self._handle_dbapi_exception(
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 2358, in _handle_dbapi_exception
    |     raise exc_info[1].with_traceback(exc_info[2])
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
    |     self.dialect.do_execute(
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/engine/default.py", line 941, in do_execute
    |     cursor.execute(statement, parameters)
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/aiomysql.py", line 95, in execute
    |     return self.await_(self._execute_async(operation, parameters))
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
    |     return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
    |     value = await result
    |             ^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/aiomysql.py", line 104, in _execute_async
    |     result = await self._cursor.execute(operation, parameters)
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/opt/conda/lib/python3.11/site-packages/aiomysql/cursors.py", line 237, in execute
    |     query = query % self._escape_args(args, conn)
    |             ~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | ValueError: unsupported format character 'Y' (0x59) at index 886
    +------------------------------------
@aldbr aldbr self-assigned this Dec 5, 2024
@aldbr aldbr changed the title Try DiracX WebApp /api/jobs/search not robust enough with DateTime? Dec 5, 2024
@ryuwd
Copy link
Contributor

ryuwd commented Dec 16, 2024

How about

class ScalarSearchSpec(TypedDict):
    parameter: str
    operator: ScalarSearchOperator
-    value: str | int
+    value: str | int | datetime

https://docs.pydantic.dev/2.2/usage/types/datetime/

@ryuwd
Copy link
Contributor

ryuwd commented Dec 16, 2024

Aha, this is caused by insufficient escaping: https://stackoverflow.com/questions/16531116/python-valueerror-unsupported-format-character-y-0x59

Probably needs to be fixed here:

@compiles(date_trunc, "mysql")
def mysql_date_trunc(element, compiler, **kw):
pattern = {
"SECOND": "%Y-%m-%d %H:%i:%S",
"MINUTE": "%Y-%m-%d %H:%i",
"HOUR": "%Y-%m-%d %H",
"DAY": "%Y-%m-%d",
"MONTH": "%Y-%m",
"YEAR": "%Y",
}[element._time_resolution]
return f"DATE_FORMAT({compiler.process(element.clauses)}, '{pattern}')"
@compiles(date_trunc, "sqlite")
def sqlite_date_trunc(element, compiler, **kw):
pattern = {
"SECOND": "%Y-%m-%d %H:%M:%S",
"MINUTE": "%Y-%m-%d %H:%M",
"HOUR": "%Y-%m-%d %H",
"DAY": "%Y-%m-%d",
"MONTH": "%Y-%m",
"YEAR": "%Y",
}[element._time_resolution]
return f"strftime('{pattern}', {compiler.process(element.clauses)})"

Probably need to ensure double-escaped %% or use some other trick

@ryuwd
Copy link
Contributor

ryuwd commented Dec 16, 2024

This works:

@compiles(date_trunc, "postgresql")
def pg_date_trunc(element, compiler, **kw):
    res = {
        "SECOND": "second",
        "MINUTE": "minute",
        "HOUR": "hour",
        "DAY": "day",
        "MONTH": "month",
        "YEAR": "year",
    }[element._time_resolution]
    return f"date_trunc('{res}', {compiler.process(element.clauses)})"


@compiles(date_trunc, "mysql")
def mysql_date_trunc(element, compiler, **kw):
    pattern = {
        "SECOND": "%Y-%m-%d %H:%i:%S",
        "MINUTE": "%Y-%m-%d %H:%i",
        "HOUR": "%Y-%m-%d %H",
        "DAY": "%Y-%m-%d",
        "MONTH": "%Y-%m",
        "YEAR": "%Y",
    }[element._time_resolution]

    dt_col, = list(element.clauses)
    return compiler.process(
        func.date_format(
            dt_col, pattern
        )
    )


@compiles(date_trunc, "sqlite")
def sqlite_date_trunc(element, compiler, **kw):
    pattern = {
        "SECOND": "%Y-%m-%d %H:%M:%S",
        "MINUTE": "%Y-%m-%d %H:%M",
        "HOUR": "%Y-%m-%d %H",
        "DAY": "%Y-%m-%d",
        "MONTH": "%Y-%m",
        "YEAR": "%Y",
    }[element._time_resolution]
    dt_col, = list(element.clauses)
    return compiler.process(
        func.strftime(
            pattern, dt_col, 
        )
    )

@ryuwd ryuwd mentioned this issue Dec 16, 2024
6 tasks
@aldbr
Copy link
Contributor Author

aldbr commented Dec 16, 2024

Great! Thanks 🙂

But there are 2 different issues here:

  • the one that completely crash diracX that you have resolved from what I understand
  • and another one bound to a lack of flexibility with the milliseconds (diracX only accepts microseconds and not milliseconds): we would need to adapt the following line (\d{6}Z to \d{3,6}Z):

r"\d{4}(-\d{2}(-\d{2}(([ T])\d{2}(:\d{2}(:\d{2}(\.\d{6}Z?)?)?)?)?)?)?", value

Have you added some tests for your change?

IIUC, we would need to add unit tests working with SQLite (in tests, it's the default DB), and with MySQL.

Note for myself: I found this package for freezing dates and prevent potential flaky unit tests dealing with date times: https://github.com/spulec/freezegun

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants