From c5246d76ab59c7c70304a89c4009fc7fefbb371c Mon Sep 17 00:00:00 2001 From: mahdi ghasemi Date: Mon, 19 Aug 2024 15:22:56 +0330 Subject: [PATCH] automation some actions --- .gitignore | 4 +- README.md | 189 +++-------------------------------- abarorm.egg-info/PKG-INFO | 190 +++--------------------------------- abarorm/fields.py | 1 + abarorm/mysql.py | 64 ++++++++++-- abarorm/orm.py | 58 +++++++++-- build/lib/abarorm/fields.py | 1 + build/lib/abarorm/orm.py | 58 +++++++++-- example.py | 14 ++- setup.py | 2 +- 10 files changed, 201 insertions(+), 380 deletions(-) diff --git a/.gitignore b/.gitignore index 5bb2a08..e4a6600 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ venv __pycache__ example.db build -dist \ No newline at end of file +dist +/dist +/build diff --git a/README.md b/README.md index 9e9dce1..5a185f1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # abarorm - -| ![abarorm Logo](abarorm/abarorm.png) | **abarorm** is a lightweight and easy-to-use Object-Relational Mapping (ORM) library for SQLite databases in Python. It aims to provide a simple and intuitive interface for managing database models and interactions. | +| ![abarorm Logo](https://prodbygodfather.github.io/abarorm/images/logo.png) | **abarorm** is a lightweight and easy-to-use Object-Relational Mapping (ORM) library for both SQLite and MySQL databases in Python. It provides a simple and intuitive interface for managing database models and interactions. | |----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + ## Features - Define models using Python classes @@ -10,6 +10,7 @@ - Support for basic CRUD operations - Foreign key relationships - Custom field types with validation and constraints +- **New in v1.0.0**: Automatic table creation and updates without needing explicit `create_table()` calls ## Installation @@ -18,188 +19,28 @@ You can install **abarorm** from PyPI using pip: ```bash pip install abarorm ``` - For MySQL support, you also need to install `mysql-connector-python`: - -```bash +```python pip install mysql-connector-python ``` ## Basic Usage -Here’s a quick overview of how to use **abarorm** to define models and interact with an SQLite database. - -## Field Types -In **abarorm**, fields define the types of data stored in your database models. You can use built-in field types to specify the kind of data each attribute should hold. Here are the available field types and their usage: - -1. **CharField** - - **Description:** Represents a text field with a maximum length. - - **Parameters:** - - `max_length` : The maximum number of characters allowed. - - `unique` : If True, the field must contain unique values across the table. - - `null` : If True, the field can contain NULL values. - - `default` : The default value if none is provided. - - **Example:** - ```python - title = CharField(max_length=100, unique=True) - ``` -2. **DateTimeField** - - **Description:** Represents a date and time value. - - **Parameters:** - - `auto_now` : If True, the field will be automatically set to the current date and time whenever the record is updated. - - **Example:** - ```python - create_time = DateTimeField(auto_now=True) - ``` -3. **ForeignKey** - - **Description:** Represents a many-to-one relationship between models. - - **Parameters:** - - `to` : The model that this field points to. - - `on_delete` : Defines the behavior when the referenced record is deleted. Common options include: - - `CASCADE` : Automatically delete records that reference the deleted record. - - `SET NULL` : Set the field to NULL when the referenced record is deleted. - - `PROTECT` : Prevent deletion of the referenced record by raising an error. - - `SET_DEFAULT` : Set the field to a default value when the referenced record is deleted. - - `DO_NOTHING` : Do nothing and leave the field unchanged. - - **Example:** - ```python - category = ForeignKey(Category, on_delete='CASCADE') - ``` -4. **BooleanField** - - **Description:** Represents a Boolean value (`True` or `False`). - - **Parameters:** - - `default` : The default value for the field if none is provided. - - `null` : if `True`, the field can contain `NULL` values. - - **Example:** - ```python - is_active = BooleanField(default=True) - ``` -5. **IntegerField** - - **Description:** Represents an integer value. - - **Parameters:** - - `default` : The default value for the field if none is provided. - - `null` : If True, the field can contain NULL values. - - **Example:** - ```py - age = IntegerField(default=0) - ``` - -## Defining Models -Create a new Python file (e.g., `models.py`) and define your models by inheriting from `SQLiteModel` for SQLite or `MySQLModel` for MySQL. Update your database configuration accordingly. - - -**Example for SQLite:** -```python -from abarorm import SQLiteModel -from abarorm.fields import CharField, DateTimeField, ForeignKey - -DATABASE_CONFIG = { - 'sqlite': { - 'db_name': 'example.db', # Name of the SQLite database file - } -} - -class Category(SQLiteModel): - table_name = 'categories' - title = CharField(max_length=200, unique=True) - def __init__(self, **kwargs): - # Initialize the Category model with database configuration - super().__init__(db_config=DATABASE_CONFIG['sqlite'], **kwargs) - -class Post(SQLiteModel): - table_name = 'posts' - title = CharField(max_length=100, unique=True) - create_time = DateTimeField(auto_now=True) - category = ForeignKey(Category) - def __init__(self, **kwargs): - # Initialize the Category model with database configuration - super().__init__(db_config=DATABASE_CONFIG['sqlite'], **kwargs) -``` -**Example for MySQL:** -```python -from abarorm import MySQLModel -from abarorm.fields import CharField, DateTimeField, ForeignKey - -DATABASE_CONFIG = { - 'mysql': { - 'host': 'localhost', - 'user': 'your_user', - 'password': 'your_password', - 'db_name': 'example_db', # MySQL database name - } -} - -class Category(MySQLModel): - table_name = 'categories' - title = CharField(max_length=200, unique=True) +Here’s a quick overview of how to use **abarorm** to define models and interact with an SQLite or MySQL database. - def __init__(self, **kwargs): - super().__init__(db_config=DATABASE_CONFIG['mysql'], **kwargs) - -class Post(MySQLModel): - table_name = 'posts' - title = CharField(max_length=100, unique=True) - create_time = DateTimeField(auto_now=True) - category = ForeignKey(Category) +## Documentation +For detailed documentation, examples, and advanced usage, please visit the [official abarorm documentation website](https://prodbygodfather.github.io/abarorm/). - def __init__(self, **kwargs): - super().__init__(db_config=DATABASE_CONFIG['mysql'], **kwargs) -``` - -## Creating Tables -Create the tables in the database by calling the `create_table` method on your model classes: - -```python -if __name__ == "__main__": - Category.create_table() - Post.create_table() -``` -## Adding Data -You can add new records to the database using the `create` method: -```python -# Adding a new category -Category.create(title='Movies') - -# Adding a new post -category = Category.get(id=1) # Fetch the category with ID 1 -if category: - Post.create(title='Godfather', category=category.id) -``` -## Querying Data -Retrieve all records or filter records based on criteria: - -```python -# Retrieve all posts -all_posts = Post.all() -print("All Posts:", [(post.title, post.category) for post in all_posts]) - -# Retrieve a specific post -post_data = Post.get(id=1) -if post_data: - print("Post with ID 1:", post_data.title, post_data.category) - -# Filter posts by category -filtered_posts = Post.filter(category=category.id) -print("Filtered Posts:", [(post.title, post.category) for post in filtered_posts]) -``` -## Updating Records -Update existing records with the `update` method: -```python -Update existing records with the update method: -``` -## Deleting Records -Delete records using the `delete` method: -```python -Post.delete(1) -``` +## Version 1.0.0 Notes +**Automatic Table Management:** Tables are now automatically created or updated based on model definitions without manual intervention. +**Important for Developers:** During development, when adding new fields to models, they will default to `NULL`. It's recommended to recreate the database schema after development is complete to ensure fields have appropriate constraints and default values. ## Contributing -Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on [github](https://github.com/prodbygodfather/abarorm). +Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on GitHub. ## License -This project is licensed under the MIT License - see the [License](https://github.com/ProdByGodfather/abarorm/blob/main/LICENSE) file for details. +This project is licensed under the Apache-2.0 [License](https://github.com/ProdByGodfather/abarorm/blob/main/LICENSE) - see the LICENSE file for details. ## Acknowledgements - -- **Python**: The language used for this project -- **SQLite**: The database used for this project -- **setuptools**: The tool used for packaging and distributing the library \ No newline at end of file +**Python:** The language used for this project +**SQLite & MySQL:** The databases supported by this project +**setuptools:** The tool used for packaging and distributing the library \ No newline at end of file diff --git a/abarorm.egg-info/PKG-INFO b/abarorm.egg-info/PKG-INFO index 0588bb6..c31c9da 100644 --- a/abarorm.egg-info/PKG-INFO +++ b/abarorm.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: abarorm -Version: 0.9.0 +Version: 1.0.0 Summary: A simple ORM library Home-page: https://github.com/prodbygodfather/abarorm Author: Mahdi Ghasemi @@ -22,7 +22,8 @@ Requires-Dist: mysql-connector-python # abarorm -**abarorm** is a lightweight and easy-to-use Object-Relational Mapping (ORM) library for SQLite databases in Python. It aims to provide a simple and intuitive interface for managing database models and interactions. +| ![abarorm Logo](https://prodbygodfather.github.io/abarorm/images/logo.png) | **abarorm** is a lightweight and easy-to-use Object-Relational Mapping (ORM) library for both SQLite and MySQL databases in Python. It provides a simple and intuitive interface for managing database models and interactions. | +|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## Features @@ -31,6 +32,7 @@ Requires-Dist: mysql-connector-python - Support for basic CRUD operations - Foreign key relationships - Custom field types with validation and constraints +- **New in v1.0.0**: Automatic table creation and updates without needing explicit `create_table()` calls ## Installation @@ -39,188 +41,28 @@ You can install **abarorm** from PyPI using pip: ```bash pip install abarorm ``` - For MySQL support, you also need to install `mysql-connector-python`: - -```bash +```python pip install mysql-connector-python ``` ## Basic Usage -Here’s a quick overview of how to use **abarorm** to define models and interact with an SQLite database. - -## Field Types -In **abarorm**, fields define the types of data stored in your database models. You can use built-in field types to specify the kind of data each attribute should hold. Here are the available field types and their usage: - -1. **CharField** - - **Description:** Represents a text field with a maximum length. - - **Parameters:** - - `max_length` : The maximum number of characters allowed. - - `unique` : If True, the field must contain unique values across the table. - - `null` : If True, the field can contain NULL values. - - `default` : The default value if none is provided. - - **Example:** - ```python - title = CharField(max_length=100, unique=True) - ``` -2. **DateTimeField** - - **Description:** Represents a date and time value. - - **Parameters:** - - `auto_now` : If True, the field will be automatically set to the current date and time whenever the record is updated. - - **Example:** - ```python - create_time = DateTimeField(auto_now=True) - ``` -3. **ForeignKey** - - **Description:** Represents a many-to-one relationship between models. - - **Parameters:** - - `to` : The model that this field points to. - - `on_delete` : Defines the behavior when the referenced record is deleted. Common options include: - - `CASCADE` : Automatically delete records that reference the deleted record. - - `SET NULL` : Set the field to NULL when the referenced record is deleted. - - `PROTECT` : Prevent deletion of the referenced record by raising an error. - - `SET_DEFAULT` : Set the field to a default value when the referenced record is deleted. - - `DO_NOTHING` : Do nothing and leave the field unchanged. - - **Example:** - ```python - category = ForeignKey(Category, on_delete='CASCADE') - ``` -4. **BooleanField** - - **Description:** Represents a Boolean value (`True` or `False`). - - **Parameters:** - - `default` : The default value for the field if none is provided. - - `null` : if `True`, the field can contain `NULL` values. - - **Example:** - ```python - is_active = BooleanField(default=True) - ``` -5. **IntegerField** - - **Description:** Represents an integer value. - - **Parameters:** - - `default` : The default value for the field if none is provided. - - `null` : If True, the field can contain NULL values. - - **Example:** - ```py - age = IntegerField(default=0) - ``` - -## Defining Models -Create a new Python file (e.g., `models.py`) and define your models by inheriting from `SQLiteModel` for SQLite or `MySQLModel` for MySQL. Update your database configuration accordingly. - - -**Example for SQLite:** -```python -from abarorm import SQLiteModel -from abarorm.fields import CharField, DateTimeField, ForeignKey - -DATABASE_CONFIG = { - 'sqlite': { - 'db_name': 'example.db', # Name of the SQLite database file - } -} - -class Category(SQLiteModel): - table_name = 'categories' - title = CharField(max_length=200, unique=True) - def __init__(self, **kwargs): - # Initialize the Category model with database configuration - super().__init__(db_config=DATABASE_CONFIG['sqlite'], **kwargs) - -class Post(SQLiteModel): - table_name = 'posts' - title = CharField(max_length=100, unique=True) - create_time = DateTimeField(auto_now=True) - category = ForeignKey(Category) - def __init__(self, **kwargs): - # Initialize the Category model with database configuration - super().__init__(db_config=DATABASE_CONFIG['sqlite'], **kwargs) -``` -**Example for MySQL:** -```python -from abarorm import MySQLModel -from abarorm.fields import CharField, DateTimeField, ForeignKey - -DATABASE_CONFIG = { - 'mysql': { - 'host': 'localhost', - 'user': 'your_user', - 'password': 'your_password', - 'db_name': 'example_db', # MySQL database name - } -} - -class Category(MySQLModel): - table_name = 'categories' - title = CharField(max_length=200, unique=True) - - def __init__(self, **kwargs): - super().__init__(db_config=DATABASE_CONFIG['mysql'], **kwargs) - -class Post(MySQLModel): - table_name = 'posts' - title = CharField(max_length=100, unique=True) - create_time = DateTimeField(auto_now=True) - category = ForeignKey(Category) +Here’s a quick overview of how to use **abarorm** to define models and interact with an SQLite or MySQL database. - def __init__(self, **kwargs): - super().__init__(db_config=DATABASE_CONFIG['mysql'], **kwargs) -``` - -## Creating Tables -Create the tables in the database by calling the `create_table` method on your model classes: +## Documentation +For detailed documentation, examples, and advanced usage, please visit the [official abarorm documentation website](https://prodbygodfather.github.io/abarorm/). -```python -if __name__ == "__main__": - Category.create_table() - Post.create_table() -``` -## Adding Data -You can add new records to the database using the `create` method: -```python -# Adding a new category -Category.create(title='Movies') - -# Adding a new post -category = Category.get(id=1) # Fetch the category with ID 1 -if category: - Post.create(title='Godfather', category=category.id) -``` -## Querying Data -Retrieve all records or filter records based on criteria: - -```python -# Retrieve all posts -all_posts = Post.all() -print("All Posts:", [(post.title, post.category) for post in all_posts]) - -# Retrieve a specific post -post_data = Post.get(id=1) -if post_data: - print("Post with ID 1:", post_data.title, post_data.category) - -# Filter posts by category -filtered_posts = Post.filter(category=category.id) -print("Filtered Posts:", [(post.title, post.category) for post in filtered_posts]) -``` -## Updating Records -Update existing records with the `update` method: -```python -Update existing records with the update method: -``` -## Deleting Records -Delete records using the `delete` method: -```python -Post.delete(1) -``` +## Version 1.0.0 Notes +**Automatic Table Management:** Tables are now automatically created or updated based on model definitions without manual intervention. +**Important for Developers:** During development, when adding new fields to models, they will default to `NULL`. It's recommended to recreate the database schema after development is complete to ensure fields have appropriate constraints and default values. ## Contributing -Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on [github](https://github.com/prodbygodfather/abarorm). +Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on GitHub. ## License -This project is licensed under the MIT License - see the [License](https://github.com/prodbygodfather/abarorm/LICENSE) file for details. +This project is licensed under the Apache-2.0 [License](https://github.com/ProdByGodfather/abarorm/blob/main/LICENSE) - see the LICENSE file for details. ## Acknowledgements - -- **Python**: The language used for this project -- **SQLite**: The database used for this project -- **setuptools**: The tool used for packaging and distributing the library +**Python:** The language used for this project +**SQLite & MySQL:** The databases supported by this project +**setuptools:** The tool used for packaging and distributing the library diff --git a/abarorm/fields.py b/abarorm/fields.py index 96c467f..7ce211c 100644 --- a/abarorm/fields.py +++ b/abarorm/fields.py @@ -32,3 +32,4 @@ def __init__(self, to: Type['BaseModel'], on_delete: str = 'CASCADE', **kwargs): super().__init__(field_type='INTEGER', **kwargs) self.to = to self.on_delete = on_delete + diff --git a/abarorm/mysql.py b/abarorm/mysql.py index f18214a..177c2c0 100644 --- a/abarorm/mysql.py +++ b/abarorm/mysql.py @@ -1,9 +1,16 @@ import mysql.connector -from typing import List, Optional, Type, Dict +from typing import List, Optional, Dict import datetime from .fields import Field, DateTimeField -class BaseModel: +class ModelMeta(type): + def __new__(cls, name, bases, dct): + new_cls = super().__new__(cls, name, bases, dct) + if 'table_name' in dct and dct['table_name']: # اگر table_name تعریف شده بود + new_cls.create_table() # به صورت اتوماتیک جدول ایجاد می‌شود + return new_cls + +class BaseModel(metaclass=ModelMeta): table_name = '' def __init__(self, **kwargs): @@ -12,13 +19,25 @@ def __init__(self, **kwargs): @classmethod def connect(cls): - # This method should be overridden by subclasses to provide the database connection - raise NotImplementedError("Connect method must be implemented.") + raise NotImplementedError("متد Connect باید پیاده‌سازی شود.") @classmethod def create_table(cls): conn = cls.connect() cursor = conn.cursor() + columns = cls._get_column_definitions(cursor) + + # ایجاد جدول اگر وجود ندارد + cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INT AUTO_INCREMENT PRIMARY KEY, {', '.join(columns)})") + + # به روزرسانی ساختار جدول در صورت نیاز + cls._update_table_structure(cursor) + + conn.commit() + conn.close() + + @classmethod + def _get_column_definitions(cls, cursor): columns = [] for attr, field in cls.__dict__.items(): if isinstance(field, Field): @@ -35,12 +54,39 @@ def create_table(cls): column_definition += f" DEFAULT '{field.default}'" else: column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" columns.append(column_definition) - if not columns: - raise ValueError("Table must have at least one field.") - cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INT AUTO_INCREMENT PRIMARY KEY, {', '.join(columns)})") - conn.commit() - conn.close() + return columns + + @classmethod + def _update_table_structure(cls, cursor): + existing_columns = cls._get_existing_columns(cursor) + new_columns = [attr for attr in cls.__dict__ if isinstance(cls.__dict__[attr], Field) and attr not in existing_columns] + + for column in new_columns: + field = cls.__dict__[column] + col_type = field.field_type + column_definition = f"ALTER TABLE {cls.table_name} ADD COLUMN {column} {col_type}" + if field.unique: + column_definition += " UNIQUE" + if field.null: + column_definition += " NULL" + else: + column_definition += " NOT NULL" + if field.default is not None: + if isinstance(field.default, str): + column_definition += f" DEFAULT '{field.default}'" + else: + column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" + cursor.execute(column_definition) + + @classmethod + def _get_existing_columns(cls, cursor): + cursor.execute(f"DESCRIBE {cls.table_name}") + return {row[0] for row in cursor.fetchall()} @classmethod def all(cls) -> List['BaseModel']: diff --git a/abarorm/orm.py b/abarorm/orm.py index 41617ba..99c8c0f 100644 --- a/abarorm/orm.py +++ b/abarorm/orm.py @@ -1,9 +1,16 @@ import sqlite3 -from typing import List, Optional, Type, Dict +from typing import List, Optional, Dict import datetime from .fields import Field, DateTimeField -class BaseModel: +class ModelMeta(type): + def __new__(cls, name, bases, dct): + new_cls = super().__new__(cls, name, bases, dct) + if 'table_name' in dct and dct['table_name']: # Check if table_name is defined + new_cls.create_table() # Automatically create the table + return new_cls + +class BaseModel(metaclass=ModelMeta): table_name = '' def __init__(self, **kwargs): @@ -12,13 +19,21 @@ def __init__(self, **kwargs): @classmethod def connect(cls): - # This method should be overridden by subclasses to provide the database connection raise NotImplementedError("Connect method must be implemented.") @classmethod def create_table(cls): conn = cls.connect() cursor = conn.cursor() + columns = cls._get_column_definitions(cursor) + + cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INTEGER PRIMARY KEY, {', '.join(columns)})") + cls._update_table_structure(cursor) + conn.commit() + conn.close() + + @classmethod + def _get_column_definitions(cls, cursor): columns = [] for attr, field in cls.__dict__.items(): if isinstance(field, Field): @@ -35,12 +50,39 @@ def create_table(cls): column_definition += f" DEFAULT '{field.default}'" else: column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" # Allow NULL by default to avoid errors columns.append(column_definition) - if not columns: - raise ValueError("Table must have at least one field.") - cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INTEGER PRIMARY KEY, {', '.join(columns)})") - conn.commit() - conn.close() + return columns + + @classmethod + def _update_table_structure(cls, cursor): + existing_columns = cls._get_existing_columns(cursor) + new_columns = [attr for attr in cls.__dict__ if isinstance(cls.__dict__[attr], Field) and attr not in existing_columns] + + for column in new_columns: + field = cls.__dict__[column] + col_type = field.field_type + column_definition = f"ALTER TABLE {cls.table_name} ADD COLUMN {column} {col_type}" + if field.unique: + column_definition += " UNIQUE" + if field.null: + column_definition += " NULL" + else: + column_definition += " NOT NULL" + if field.default is not None: + if isinstance(field.default, str): + column_definition += f" DEFAULT '{field.default}'" + else: + column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" # Allow NULL by default + cursor.execute(column_definition) + + @classmethod + def _get_existing_columns(cls, cursor): + cursor.execute(f"PRAGMA table_info({cls.table_name})") + return {row[1] for row in cursor.fetchall()} @classmethod def all(cls) -> List['BaseModel']: diff --git a/build/lib/abarorm/fields.py b/build/lib/abarorm/fields.py index 96c467f..7ce211c 100644 --- a/build/lib/abarorm/fields.py +++ b/build/lib/abarorm/fields.py @@ -32,3 +32,4 @@ def __init__(self, to: Type['BaseModel'], on_delete: str = 'CASCADE', **kwargs): super().__init__(field_type='INTEGER', **kwargs) self.to = to self.on_delete = on_delete + diff --git a/build/lib/abarorm/orm.py b/build/lib/abarorm/orm.py index 41617ba..99c8c0f 100644 --- a/build/lib/abarorm/orm.py +++ b/build/lib/abarorm/orm.py @@ -1,9 +1,16 @@ import sqlite3 -from typing import List, Optional, Type, Dict +from typing import List, Optional, Dict import datetime from .fields import Field, DateTimeField -class BaseModel: +class ModelMeta(type): + def __new__(cls, name, bases, dct): + new_cls = super().__new__(cls, name, bases, dct) + if 'table_name' in dct and dct['table_name']: # Check if table_name is defined + new_cls.create_table() # Automatically create the table + return new_cls + +class BaseModel(metaclass=ModelMeta): table_name = '' def __init__(self, **kwargs): @@ -12,13 +19,21 @@ def __init__(self, **kwargs): @classmethod def connect(cls): - # This method should be overridden by subclasses to provide the database connection raise NotImplementedError("Connect method must be implemented.") @classmethod def create_table(cls): conn = cls.connect() cursor = conn.cursor() + columns = cls._get_column_definitions(cursor) + + cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INTEGER PRIMARY KEY, {', '.join(columns)})") + cls._update_table_structure(cursor) + conn.commit() + conn.close() + + @classmethod + def _get_column_definitions(cls, cursor): columns = [] for attr, field in cls.__dict__.items(): if isinstance(field, Field): @@ -35,12 +50,39 @@ def create_table(cls): column_definition += f" DEFAULT '{field.default}'" else: column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" # Allow NULL by default to avoid errors columns.append(column_definition) - if not columns: - raise ValueError("Table must have at least one field.") - cursor.execute(f"CREATE TABLE IF NOT EXISTS {cls.table_name} (id INTEGER PRIMARY KEY, {', '.join(columns)})") - conn.commit() - conn.close() + return columns + + @classmethod + def _update_table_structure(cls, cursor): + existing_columns = cls._get_existing_columns(cursor) + new_columns = [attr for attr in cls.__dict__ if isinstance(cls.__dict__[attr], Field) and attr not in existing_columns] + + for column in new_columns: + field = cls.__dict__[column] + col_type = field.field_type + column_definition = f"ALTER TABLE {cls.table_name} ADD COLUMN {column} {col_type}" + if field.unique: + column_definition += " UNIQUE" + if field.null: + column_definition += " NULL" + else: + column_definition += " NOT NULL" + if field.default is not None: + if isinstance(field.default, str): + column_definition += f" DEFAULT '{field.default}'" + else: + column_definition += f" DEFAULT {field.default}" + else: + column_definition += " DEFAULT NULL" # Allow NULL by default + cursor.execute(column_definition) + + @classmethod + def _get_existing_columns(cls, cursor): + cursor.execute(f"PRAGMA table_info({cls.table_name})") + return {row[1] for row in cursor.fetchall()} @classmethod def all(cls) -> List['BaseModel']: diff --git a/example.py b/example.py index e243dbb..643800e 100644 --- a/example.py +++ b/example.py @@ -8,13 +8,15 @@ } } + # Define the Category model class Category(SQLiteModel): table_name = 'categories' # Name of the table in the database # Define the fields of the Category model - title = CharField(max_length=200, unique=True, null=False) # Title of the category, must be unique and not null - + title = CharField(max_length=200, null=False) # Title of the category, must be unique and not null + create_time = DateTimeField(auto_now=True) # Creation time of the category, automatically set to current datetime + update_time = DateTimeField(auto_now=True) def __init__(self, **kwargs): # Initialize the Category model with database configuration super().__init__(db_config=DATABASE_CONFIG['sqlite'], **kwargs) @@ -24,7 +26,7 @@ class Post(SQLiteModel): table_name = 'posts' # Name of the table in the database # Define the fields of the Post model - title = CharField(max_length=100, unique=True, null=False) # Title of the post, must be unique and not null + title = CharField(max_length=100, null=False) # Title of the post, must be unique and not null create_time = DateTimeField(auto_now=True) # Creation time of the post, automatically set to current datetime category = ForeignKey(to=Category) # Foreign key referring to the Category model @@ -35,8 +37,8 @@ def __init__(self, **kwargs): # Main execution block if __name__ == "__main__": # Create tables in the database - Category.create_table() # This will create the 'categories' table - Post.create_table() # This will create the 'posts' table + # Category.create_table() # This will create the 'categories' table + # Post.create_table() # This will create the 'posts' table # Create a new category Category.create(title='Movies') # Add a new category with title 'Movies' @@ -49,7 +51,9 @@ def __init__(self, **kwargs): # Read all posts all_posts = Post.all() # Retrieve all posts from the database + all_categoires = Category.all() print("All Posts:", [(post.title, post.category) for post in all_posts]) # Print all posts with their titles and associated categories + print("All Categories:", [(post.title, post.create_time, post.update_time) for post in all_categoires]) # Print all posts with their titles and associated categories # Use the get method to retrieve a post by ID post_data = Post.get(id=1) # Fetch the post with ID 1 diff --git a/setup.py b/setup.py index 22d7913..c5e16ef 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='abarorm', - version='0.9.0', + version='1.0.0', description='A simple ORM library', long_description=open('README.md').read(), long_description_content_type='text/markdown',