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

Add SQLAlchemy Helpers integration #23

Merged
merged 1 commit into from
Jul 9, 2024
Merged

Add SQLAlchemy Helpers integration #23

merged 1 commit into from
Jul 9, 2024

Conversation

gridhead
Copy link
Member

@gridhead gridhead commented Jul 2, 2024

Add SQLAlchemy Helpers integration

@gridhead gridhead added the enhancement New feature or request label Jul 2, 2024
@gridhead gridhead requested a review from abompard July 2, 2024 04:26
@gridhead gridhead self-assigned this Jul 2, 2024
is_sqlite,
)
from sqlalchemy_helpers import Base, get_or_create, update_or_create, is_sqlite, exists_in_db
from sqlalchemy_helpers.flask_ext import DatabaseExtension, get_or_404, first_or_404
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those lines are causing ruff/flake8 error F401, which is normal, but please add the comment to ignore it.

@@ -42,7 +44,7 @@ def run_migrations_offline() -> None:
script output.

"""
url = str(get_config().database.sqlalchemy.url)
url = str(get_url_from_app(create_app))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's another line down this file where get_config() should be replaced, otherwise things crash.

The docs recommend getting the URL once and setting it in alembic's config object:

url = get_url_from_app(create_app)
config.set_main_option("sqlalchemy.url", url)

And then you can use the regular alembic template for the env.py file downwards.

Or you could get the URL in a variable in the top-level section, and replace the engine_from_config call by a simple create_engine call that would use this variable.

@abompard
Copy link
Member

abompard commented Jul 2, 2024

FYI you can test database creation with:

W2FM_APPCONFIG=your-config-file.toml poetry run flask -A webhook_to_fedora_messaging.main db sync

Copy link
Member

@abompard abompard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the db sync command I provided work for you now? For me it still doesn't.

@gridhead gridhead force-pushed the sqla branch 2 times, most recently from f6079c9 to 3b3eaff Compare July 3, 2024 05:53
@gridhead
Copy link
Member Author

gridhead commented Jul 3, 2024

@abompard It does not work here either and I could use some help with defining TZ-aware objects I am currently doing it like this https://github.com/fedora-infra/webhook-to-fedora-messaging/pull/23/files#diff-d89f48631b32644376fe0ac1a41a9197091ddbdbeb10f08a437278bab2fbfec4R23-R25 but I suspect that is a mistaken approach.

Following is a truncated traceback.

Traceback (most recent call last):
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/bin/flask", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/flask/cli.py", line 1105, in main
    cli.main()
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/flask/cli.py", line 386, in decorator
    return ctx.invoke(f, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy_helpers/flask_ext.py", line 32, in _syncdb
    result = manager.sync()
             ^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy_helpers/manager.py", line 172, in sync
    self.create()
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy_helpers/manager.py", line 120, in create
    self._base_model.metadata.create_all(bind=self.engine)
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/schema.py", line 5857, in create_all
    bind._run_ddl_visitor(
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 3251, in _run_ddl_visitor
    conn._run_ddl_visitor(visitorcallable, element, **kwargs)
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 2457, in _run_ddl_visitor
    visitorcallable(self.dialect, self, **kwargs).traverse_single(element)
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 664, in traverse_single
    return meth(obj, **kw)
           ^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 918, in visit_metadata
    self.traverse_single(
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 664, in traverse_single
    return meth(obj, **kw)
           ^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 956, in visit_table
    )._invoke_with(self.connection)
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 314, in _invoke_with
    return bind.execute(self)
           ^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1418, in execute
    return meth(
           ^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 180, in _execute_on_connection
    return connection._execute_ddl(
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1526, in _execute_ddl
    compiled = ddl.compile(
               ^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/elements.py", line 308, in compile
    return self._compiler(dialect, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 69, in _compiler
    return dialect.ddl_compiler(dialect, self, **kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 870, in __init__
    self.string = self.process(self.statement, **compile_kwargs)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 915, in process
    return obj._compiler_dispatch(self, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 141, in _compiler_dispatch
    return meth(self, **kw)  # type: ignore  # noqa: E501
           ^^^^^^^^^^^^^^^^
  File "/home/archdesk/Projects/webhook-to-fedora-messaging/venv/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 6655, in visit_create_table
    raise exc.CompileError(
sqlalchemy.exc.CompileError: (in table 'users', column 'creation_date'): Compiler <sqlalchemy.dialects.sqlite.base.SQLiteCompiler object at 0x7d80442de390> can't render element of type <class 'webhook_to_fedora_messaging.models.util.utcnow'>: <class 'webhook_to_fedora_messaging.models.util.utcnow'> construct has no default compilation handler.

@gridhead
Copy link
Member Author

gridhead commented Jul 3, 2024

Ok I got over the previous part but now I have a question - What pattern is the first parameter to the engine_from_config expecting and can I get an example of the reference made here

get_config().database.sqlalchemy.model_dump(),
? My get_config() is very different as it simply returns a dictionary - which won't suffice here.

@gridhead
Copy link
Member Author

gridhead commented Jul 3, 2024

Ok ignore everything I mentioned alright? It works and the solution was as easy as replacing the prefix parameters. (Jeez, I cannot believe I did not see that from the get-go)

W2FM_APPCONFIG=webhook_to_fedora_messaging/config/config.toml poetry run flask -A webhook_to_fedora_messaging.main db sync
[W2FM] [2024-07-03 12:33:32 +0530] - alembic.runtime.migration - INFO - Context impl SQLiteImpl.
[W2FM] [2024-07-03 12:33:32 +0530] - alembic.runtime.migration - INFO - Will assume non-transactional DDL.
[W2FM] [2024-07-03 12:33:32 +0530] - alembic.runtime.migration - INFO - Context impl SQLiteImpl.
[W2FM] [2024-07-03 12:33:32 +0530] - alembic.runtime.migration - INFO - Will assume non-transactional DDL.
[W2FM] [2024-07-03 12:33:32 +0530] - alembic.runtime.migration - INFO - Running stamp_revision  -> initial
Database created.

No dialects added yet so sqlite:///:memory: is taken as the default location.

@gridhead gridhead requested a review from abompard July 3, 2024 08:03
Copy link
Member

@abompard abompard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there! :-)
I'm pretty sure there are unused imports, it may be worthwhile to run your code through poetry run ruff check webhook_to_fedora_messaging for the usual stuff.

@@ -35,13 +31,11 @@ def create_app():

# Then load the variables up from the custom configuration file
try:
confdata = get_config()
confdata = get_config()
main.config.from_mapping(confdata)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you expect the next three lines to raise ConfigError? If not, let's get them out of the block.

confdata = get_config()
confdata = get_config()
main.config.from_mapping(confdata)
db.init_app(main)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be run inside the try/except block, or could this call raise ConfigError?

confdata = get_config()
main.config.from_mapping(confdata)
db.init_app(main)
dictConfig(confdata["logsconf"])
except ConfigError as expt:
logging.error(f"Exiting - Reason - {str(expt)}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It says "exiting" but it's neither exiting nor raising the exception. I think you should add a raise here so that the exception is propagated upwards the call stack.

@compiles(utcnow, "postgresql")
def _postgresql_utcnow(element, compiler, **kwargs):
return "(NOW() AT TIME ZONE 'utc')"
return str(datetime.now(UTC))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the postgresql driver will be very happy if we give it a string instead of a datetime. The SQLite driver will probably not mind though, because it's storing datetimes as strings anyway.

As a result, the utcnow function will probably boil down to functools.partial(datetime.now, tz=UTC). But you can keep it as a function either, I don't have reasons to think one is better than the other :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not completely understand the request here. Should I revert the _postgresql_utcnow function for use with the Postgres compiler and keep the string converted datetime.now(UTC) object for Sqlite cases? I removed the utcnow() function for now for a oneliner string converted functools.partial(datetime.now, tz=UTC) but I am 100% sure that would not satiate the Postgres compiler.

@gridhead gridhead force-pushed the sqla branch 2 times, most recently from 9b17470 to 9f72c12 Compare July 8, 2024 03:17
@gridhead gridhead requested a review from abompard July 8, 2024 08:40
@@ -0,0 +1,60 @@
[flaskapp]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this file used? Are those the defaults? Is it an example file? Please add a comment at the top to explain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh. This got added to the commit by mistake. I will remove this. The example config is already there in the project root.

@@ -37,4 +24,4 @@ class CreatableMixin:
An SQLAlchemy mixin to store the time when an entity was created
"""

creation_date = Column("creation_date", SQLDateTime, nullable=False, server_default=utcnow())
creation_date = Column("creation_date", SQLDateTime, nullable=False, server_default=str(partial(datetime.now, tz=UTC)))
Copy link
Member

@abompard abompard Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not going to work, here is the table schema that is generated:

sqlite> .schema services
CREATE TABLE services (
        id INTEGER NOT NULL, 
        user_id INTEGER NOT NULL, 
        name TEXT NOT NULL, 
        type TEXT NOT NULL, 
        "desc" TEXT NOT NULL, 
        disabled BOOLEAN NOT NULL, 
        uuid TEXT NOT NULL, 
        creation_date DATETIME DEFAULT 'functools.partial(<built-in method now of type object at 0x7fd402005820>, tz=datetime.timezone.utc)' NOT NULL, 
        CONSTRAINT pk_services PRIMARY KEY (id), 
        CONSTRAINT fk_services_user_id_users FOREIGN KEY(user_id) REFERENCES users (id) ON DELETE CASCADE, 
        CONSTRAINT uq_services_uuid UNIQUE (uuid)
);

We could however use default=partial(datetime.now, tz=UTC) instead of server_default, since it expects a python callable.

TEMPLATES_AUTO_RELOAD = ""
MAX_COOKIE_SIZE = ""

[flaskapp.database]
Copy link
Member

@abompard abompard Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not be used anymore, sqlalchemy-helpers uses a config key named SQLALCHEMY_DATABASE_URI (like flask-sqlalchemy)

This is also true for the config.toml.example file at the root of the project.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. Nuking the entire database section of the example config.

@@ -23,13 +23,6 @@ PREFERRED_URL_SCHEME = "http"
TEMPLATES_AUTO_RELOAD = ""
MAX_COOKIE_SIZE = ""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add the SQLALCHEMY_DATABASE_URI variable with an example value of, say, sqlite:////tmp/w2fm.db, so that people have an example of how to configure the database.
I would put it close to the top because it's pretty important.

Copy link
Member

@abompard abompard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look good, thanks! :-)

@abompard abompard merged commit 1f41356 into main Jul 9, 2024
4 of 10 checks passed
@abompard abompard deleted the sqla branch July 9, 2024 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants