My Blog

Post List

Hey there, thanks for visiting this section. I express myself here on topics which interest me.

Create a simple desktop app in Pyside and PyQt in Python with executable file

Posted on 22nd October, 2022

Pyside6 is a UI framework for Python similar to Tkinter. It is based on QT Framework originally written for C++. In this concise post we would create an elementary PySide6 UI app and create an exe for it.

It is always a good practice to create a virtual environment which would house your Python packages before you begin your project. So let's start with creating a virtual environment first and activating it. I'd be developing this app in windows.


- python -m venv venv
- cd venv/Scripts
- activate.bat

Next step would be to install packages, we would only install what is required for the very primitive version of the app to run. Let's install pyinstaller and Pyside6 packages.


pip install PySide6
pip install pyinstaller



from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel

# Only needed for access to command line arguments
import sys

app = QApplication(sys.argv)

# Create a Qt widget, which will be our window.
window = QWidget()
layout = QVBoxLayout()
labelWindow = QLabel("Another Window")
layout.addWidget(labelWindow)
window.setLayout(layout)

window.show()

# Start the event loop.
app.exec_()

We have created a simple PySide6 app above. This is a very elementary app which only contains a label and a window. I'd explain now what each of those lines do. QApplication is responsible for creation of main application window. It calls the exec_() method which runs the desktop app.

QWidget is the canvas of the main app where we have a layout by creating an object from the QLayout class of the library. We are using a vertical layout through QVBoxLayout. After layout creation we have simply added a label. We can run this app now by running this file. Let's save this file as 'main.py'

Now try running this Python script. You should see a pop up window opened with text 'Another Window'. This is your PySide6 app, a very basic one indeed. Our focus for now is to be able to open this app as an executable on Windows. For this type the following command :-


pyinstaller --onefile --windowed main.py

Pyinstaller is the library we would use to bundle this python program along with it's dependencies into a single executable file which you would be able to open as a normal app on Windows. I'd briefly explain what those '--onefile' and '--windowed' parameters meant while converting Python file into executable. One file command means that there should only be a single file, if we skip this, you would have several DLL files along with the executable file. Windowed means we do not wish command line to be opened while we open our app. Sometimes, this might be a desirable behaviour if we plan to pass command line arguments to the app, but in this case we would avoid that.

You should now see a folder named 'Dist' in the same app directory which would have your exe. It should run the program as executable file if everything went fine. I haven't explored how this build works on Linux operating system, but might add that case in future. The above script is tested in Windows and seems to work just fine. With that I'd be wrapping up for today. Thanks for the read!

View

File Upload with Database in Fast API

Posted on 23rd October, 2022

FASTApi is a light weight and fast back-end framework written in Python used to create professional APIs quickly. It has support for automatic API documentation and follows Python type hinting.

In this post we would be covering slightly complex file upload feature implementation in FAST Api with database connection and models. We would be saving the file in the same project folder and would store the file path in the database. We would start the proceedings by creating a virtual environment. We would then add FastAPI and relevant packages like uvicorn (to use as a server) and create a hello world api route. All these things would be done in Windows operating system.


- python -m venv venv
- cd venv/Scripts
- activate.bat

Create a main.py file and write the following code.

from fastapi import FastAPI, Request
import uvicorn

app = FastAPI(title="Test Fast API",
    docs_url="/docs",
    version="0.0.1")

@app.get("/")
async def main(request: Request):
    return {
        'message': 'Hello World API'
    }
    
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

You can now try hitting port 8000 on your system after running main.py file. It should print the 'Hello World' message in JSON format. You can also try visiting /docs end-point which would show you the automated API docs based on Swagger specifications. It is one of the coolest FastAPI features. Let us install some more packages and work on database settings.


pip install psycopg2
pip install alembic
pip install SQLAlchemy

We would be using Postgres and PgAdmin 14 on Windows platform for demonstration. Make sure you have Postgres installed along with GUI supporting tool - PgAdmin. SQLAlchemy library is used to perform database queries, migrations that is changes to the table are taken care of by 'Alembic' package. Next step would be to link our app with Alembic and create a migrations folder.


alembic init alembic

This would create a migrations folder named 'alembic' and a config file 'alembic.ini' inside your project directory. Purpose of this config file is to let the app know the location of the migrations. We would be avoid making any advanced changes in this file and leave it as it is. Moving forward, let us now create a separate config file which would hold database settings for our app.


DATABASE_USERNAME = 'postgres'
DATABASE_PASSWORD = 'pass12345'
DATABASE_HOST = '127.0.0.1'
DATABASE_NAME = 'fast-book'

We would need to open PgAdmin and create database named 'fast-book'. Of course it could be any name you want to give, just check database name, user and password settings match with the one you created using PgAdmin. We would create a 'db.py' file and use these settings to establish database connection in our app.


from sqlalchemy import create_engine, MetaData
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

import config

DATABASE_USERNAME = config.DATABASE_USERNAME
DATABASE_PASSWORD = config.DATABASE_PASSWORD
DATABASE_HOST = config.DATABASE_HOST
DATABASE_NAME = config.DATABASE_NAME

SQLALCHEMY_DATABASE_URL = f"postgresql://{DATABASE_USERNAME}:{DATABASE_PASSWORD}@{DATABASE_HOST}/{DATABASE_NAME}"

engine = create_engine(SQLALCHEMY_DATABASE_URL)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
metadata = MetaData()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

We have created a database engine using the imported config settings. Then, we created a session and meta data object. Meta data object would help in keeping track of changes in the database through migrations. We would import these settings in other files, create a session, perform database operations and then close the session. Next, we would work on database models and fields. Conventionally, we put our model files inside a folder named 'models'. So let's create the folder and put 'photo.py' file inside it. We are naming our model as 'Photo' here which would have three fields, automatically generated ID, location and name.


from sqlalchemy import Column, String, Integer

from db import Base

class Photo(Base):
    __tablename__ = "photos"

    id = Column(Integer, primary_key=True, autoincrement=True)
    location = Column(String(50))
    name = Column(String(50))

Next step is to edit the env file inside the 'alembic' folder. This is done to keep track of automatic changes in the database and generate tables.


from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool
import config as dbConfig

from alembic import context
from db import Base

from models.photo import Photo

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

def get_url():
    db_user = dbConfig.DATABASE_USERNAME
    db_password = dbConfig.DATABASE_PASSWORD
    db_host = dbConfig.DATABASE_HOST
    db_name = dbConfig.DATABASE_NAME
    return f"postgresql://{db_user}:{db_password}@{db_host}/{db_name}"

def run_migrations_offline():
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = get_url()
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()


def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    configuration = config.get_section(config.config_ini_section)
    configuration["sqlalchemy.url"] = get_url()
    connectable = engine_from_config(
        configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection, target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()


Most of the settings in this file would be been generated automatically when we linked our application with Alembic. However, we have tweaked certain settings. We are importing database settings from the config file which we created earlier. We have used target_metadata to include support for automatic database table creation based on the structure of the model files. We have also included photo model in this file, this would help detecting any changes you do in future once initial database tables have been setup. We would autogenerate migrations now by typing the following command in the terminal.


alembic revision --autogenerate -m "Added photo table"

This would generate migrations and record them in the database. But, note that actual 'photos' table is not yet created as we haven't applied the created migrations yet. You would be able to see the migrations table in the database. This also creates a migrations file inside the versions directory in alembic folder. For each change you make in a database, an additional migration file is created to keep track of the changes done in database. This post is assuming you have some knowledge about database changes and migration files.


alembic upgrade head

The above command would finally create the 'photos' table whose structure would be based on the 'photo.py' file we defined earlier. You can verify this by opening PgAdmin in your browser and checking the tables for 'fast-book' (or whatever name you gave to the database) database. We are now ready to implement file upload feature in our application. We would create a separate folder named 'services'. This would have a 'photo.py' file which would contain functions to handle creation of database entries.


from fastapi import HTTPException, status
from typing import List
from models.photo import Photo

async def create_new_photo(name, location, database) -> Photo:

    new_photo = Photo(location=location, name=name)
    database.add(new_photo)
    database.commit()
    database.refresh(new_photo)
    return new_photo

I've written a function to handle POST requests to the end-point which we would create later and use for file-upload. As you can see above FASTApi uses return types hint supported in Python versions 3.5 and above. A post request would return one single object which was created with the request. We would now create a folder named 'media' which would house all our media files. We would then also connect this media to our site url so that we can see navigate to the contents of this folder through browser. We would import these functions inside 'services.py' file into 'main.py' file.


from fastapi import FastAPI, Request, Form, UploadFile, File, Depends
from services.photo import create_new_photo, get_all_photos
import uvicorn
from sqlalchemy.orm import Session
from starlette.staticfiles import StaticFiles
from models.photo import Photo

import db

app = FastAPI(title="Test Fast API",
    docs_url="/docs",
    version="0.0.1")

@app.get("/")
async def main(request: Request):
    return {
        'message': 'Hello World API'
    }

@app.post("/photo")
async def create_upload_file(name: str = Form(...), file: UploadFile = File(...), database: Session = Depends(db.get_db)):
    if not file:
        return {"message": "No upload file sent"}
    else:
        file_location = f"media/{file.filename}"
        with open(file_location, "wb+") as file_object:
            file_object.write(file.file.read())
        new_photo = await create_new_photo(name, file_location, database)
        return new_photo


@app.get('/photo')
async def get_all_photos(database: Session = Depends(db.get_db)):
    photos = database.query(Photo).all()
    return photos

app.mount("/media", StaticFiles(directory="media"), name="media")
    
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

I have modified our 'main.py' file. This is the finalized version which we would make useful in a moment by decoding the changes done line-by-line. First, we need to install a package called 'python-multipart'. This supports file upload in FastAPI as the classes defined for file upload in FastAPI depend on this package. So, let's install this package.


pip install python-multipart 

We have created a new end-point called '/photo' in our app. This would accept GET and POST requests for files through asynchronous functions. Hitting the API with a post request on this url would upload the given file inside the 'media' folder. After this, it would pass the filename in the 'create_new_photo' function which we defined inside the 'services' folder.


from starlette.staticfiles import StaticFiles

app.mount("/media", StaticFiles(directory="media"), name="media")

I'd take a moment to explain the purpose of the above two lines we added. This would make sure the files under the 'media' folder are accessible through the url. If we skip this, even though file upload would work but we would not be able to access the uploaded files through the browser. You can test this end-point using a tool called 'Postman'. Try to hit '/photo' url with form data request with 'file' and 'name' fields. You can get all the uploaded files with their names by making a GET request on the same url. With this, I'd conclude this blog. Hope you found this useful, thanks for the read.

API Docs

Automated generated docs for API by FAST Api

Postman Request

Formdata request in Postman

View

Python vs Javascript - Desktop App Development

Posted on 18th December, 2022

I'm working as a Software Engineer for quite a while now. I started exclusively with the development of web-based applications. Part of my work these days involves contributing to the development of desktop-based applications. As a result, I had exposure to multiple tools available for this in the languages already known to me - Python and Javascript. I'd be sharing my experiences in using these languages and associated tools for developing desktop-based applications in this post. At the time of writing this post, I've tried PyQt/PySide, Tkinter and Electron frameworks to create desktop-based applications. I'd be discussing my experiences of working with them, about their pros and cons in subsequent paragraphs.

I started with Tkinter, it comes installed with your Python version. Perhaps the easiest to start with, but becomes difficult to manage as the complexity of your applications increases. There are concepts like widgets, frames, canvas, layouts, animations and more which are common regardless of what framework you use. Tkinter could be a nice point for kick-starting things and familiarising yourself with desktop app development. It gets hard to adequately place your widgets (components like list-box, buttons and more) as your Tkinter app grows. Design-wise, it is primitive, not really all that good looking. It does however has some themes to choose from which would enhance the look and feel of your application. The list of various components available for you to use is very limited. For instance, tables are not available in Tkinter and to implement one in your Tkinter app, you need to adopt a hackish way of accomplishing it by using Frames, Buttons and Labels. My first full fledge computer science project was in Tkinter, but I rarely use it these days ever since I encountered PySide/PyQt library. It is still good for small apps, you don't need to install additional packages and has a soft learning curve.

I've been using PyQT/Pyside for nearly half a year. It is based on the original QT library written for C++ language. It inherits most of the functions from the C++ implementation, for the documentation, I usually refer to the C++ one since that is more descriptive. It has a wide range of components to offer from list-box, tables, spinners, sliders and more. It has a steep learning curve given the range of features it offers. It has support for QT designer which lets you create widgets through a drag-and-drop interface and then convert them to a supported Python file which you can then modify for event handling and other stuff. It makes layout management easier to handle multiple components. It has CSS-based style support for all the widgets which you can also configure in the designer view. On a personal note, I enjoy this QT designer support a lot. Makes it so much easier to get started with the design. For the negatives, there isn't much I can think of. But, the libraries do take up a lot of space compared to Tkinter. Even the final build is bulky in size compared to the one generated by Tkinter.

Electron is a library in Javascript which aims at creating native desktop-based applications using a web-development coding style. You can use React (Vue or any other Javascript library) to create your desktop application. It makes us very easy for someone like me who is coming from a web-based background in software development. Many of the popular tools are created using Electron - for example, VS Code, one of our favorite coding editors is built using Electron. The views are written and styled in CSS, event handling is done in Javascript. As a result, it provides more flexibility to you in terms of changing the UI since you can use any popular UI library lit like Material UI, Bootstrap, Chakra UI and Vuetify just to name a few to create your desktop application. I haven't been working with Electron lately but would like to continue experimenting with it in the future.

My final verdict in short would be PySide > Electron > Tkinter. I might prefer Electron future given the friendly support it has for web developers. But, for now I am leaning towards PySide. PySide and PyQt are essentially the same except that PySide is authorized to be used to create production-grade open-source projects. But, PyQT is restricted for commercial usage only after buying a subscription.

View