diff --git a/README.md b/README.md index 014c4ac..97d2d6e 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,82 @@ # Forger -A Python tool for generating fake data and exporting it to JSON files. Built with Faker, Typer, and Poetry. +A Python CLI tool for generating fake data and exporting it to JSON files. -## Description +## Overview -Forger is a command-line tool that helps you generate realistic fake data for testing and development purposes. It uses the Faker library to create various types of fake data and exports them to JSON files. +Forger helps you generate realistic test data using the Faker library. It's perfect for developers who need to quickly create test datasets for their applications. ## Features -- Generate fake user data -- Export data to JSON format -- Customizable data generation -- Command-line interface using Typer +- Generate fake user data with customizable fields +- Export to JSON format +- Support for multiple data types (names, addresses, emails) +- Command-line interface with Typer +- Configurable output format +- Batch processing ## Requirements -- Python 3.13 or higher -- Poetry for dependency management +- Python 3.13+ +- Poetry +- Git -## Installation +## Quick Start -1. Clone the repository: +1. Clone and install: ```bash git clone https://github.com/yourusername/forger.git cd forger +poetry install ``` -2. Install dependencies using Poetry: +2. Verify installation: ```bash -poetry install +forger --help ``` ## Usage -After installation, you can use Forger through the command line: - ```bash -# Generate fake user data -poetry run python main.py generate-users --count 10 --output users.json +# Basic usage +forger generate-users --count 10 --output users.json + +# Custom fields +forger generate-users --count 5 --fields name,email,address --output custom_users.json + +# Specific locale +forger generate-users --count 3 --locale fr_FR --output french_users.json ``` ## Development -This project uses Poetry for dependency management. To add new dependencies: +1. Fork and clone the repository +2. Create a feature branch: `git checkout -b feature/your-feature` +3. Install dependencies: `poetry install` +4. Run tests: `poetry run pytest` + +### Adding Dependencies ```bash +# Production dependency poetry add package-name + +# Development dependency +poetry add --group dev package-name ``` -## License +## Contributing -This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Open a Pull Request -## Author +## License -- weyderfs (weyderfs@gmail.com) +Apache 2.0 License - see [LICENSE](LICENSE) for details. -## Contributing +## Support -Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file +Open an issue in the GitHub repository for questions or problems. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 2370e10..43f3681 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,33 @@ # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "datetime" version = "5.5" @@ -31,6 +59,58 @@ files = [ [package.dependencies] tzdata = "*" +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytz" version = "2025.2" @@ -43,6 +123,25 @@ files = [ {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "setuptools" version = "80.7.1" @@ -64,6 +163,48 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "typer" +version = "0.15.4" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173"}, + {file = "typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3"}, +] + +[package.dependencies] +click = ">=8.0.0,<8.2" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] + [[package]] name = "tzdata" version = "2025.2" @@ -134,4 +275,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = ">=3.13, <4.0" -content-hash = "b71c9bbc22a949a50a21231ebc6731a6d34bd558e14e4b9f001ae00addabdf85" +content-hash = "19c68cda3af9dea7407aa4e388c28a859f9cb7225b6a0385429bf632a2cb8ba9" diff --git a/pyproject.toml b/pyproject.toml index 6911fd8..7779902 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,13 @@ requires-python = ">=3.13, <4.0" dependencies = [ "faker (>=37.3.0,<38.0.0)", "datetime (>=5.5,<6.0)", + "typer (>=0.9.0,<1.0.0)", + "rich (>=13.7.0,<14.0.0)", ] +[project.scripts] +forger = "src.cli:app" + [tool.poetry] packages = [ { include = "src" } diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..9a1f975 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,5 @@ +""" +Forger - A tool for generating fake user data +""" + +__version__ = "0.0.1" \ No newline at end of file diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..d70e3ab --- /dev/null +++ b/src/cli.py @@ -0,0 +1,43 @@ +import typer +from rich.console import Console +from rich.panel import Panel +from .services.user_generator import save_users_to_file + +# Initialize Typer app and Rich console +app = typer.Typer(help="Generate fake user data with customizable options") +console = Console() + + +@app.command() +def main( + count: int = typer.Option( + 10, + "--count", "-c", + help="Number of fake users to generate", + min=1 + ), + output: str = typer.Option( + 'users.json', + "--output", "-o", + help="Output JSON file path" + ) +) -> None: + """Generate fake user data and save it to a JSON file. + + If no arguments are provided, generates 10 users and saves to users.json + """ + try: + if not output.endswith('.json'): + output += '.json' + + save_users_to_file(count, output) + + # Display success message in a nice panel + console.print(Panel( + f"[green]Successfully generated {count} users and saved to {output}[/green]", + title="Forger", + border_style="green" + )) + except Exception as e: + console.print(f"[red]Error: {str(e)}[/red]") + raise typer.Exit(1) \ No newline at end of file diff --git a/src/main.py b/src/main.py index cdd9f09..3e7491c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,154 +1,4 @@ -import json -from datetime import datetime -from faker import Faker -from typing import List, Dict, Any -from pathlib import Path - - -# Module-level constants -DEFAULT_OUTPUT_FILE = 'user_fake.json' -DEFAULT_USER_COUNT = 10 -MIN_AGE = 18 -MAX_AGE = 99 -PASSWORD_LENGTH = 12 -GENDER_OPTIONS = ('male', 'female', 'other') - - -class FakerUser: - """A class to generate fake user data using the Faker library. - - This class generates random user data including personal information, - contact details, and account status. All data is generated using the - Faker library to ensure realistic and diverse test data. - - Attributes: - id (str): Unique identifier for the user - name (str): User's first name - middle_name (str): User's middle name - last_name (str): User's last name - email (str): User's email address - password (str): User's password - gender (str): User's gender - age (int): User's age - is_active (bool): User's account status - created_at (datetime): Account creation timestamp - updated_at (datetime): Last update timestamp - """ - - def __init__(self): - """Initialize a new FakerUser with random data.""" - fake = Faker() - self.id = fake.uuid4() - self.name = fake.first_name() - self.middle_name = fake.first_name() - self.last_name = fake.last_name() - email_base = f"{self.name.lower()}.{self.last_name.lower()}" - self.email = f"{email_base}@example.com" - self.password = fake.password(length=PASSWORD_LENGTH) - self.gender = fake.random_element(elements=GENDER_OPTIONS) - self.age = fake.random_int(min=MIN_AGE, max=MAX_AGE) - self.is_active = True - self.created_at = datetime.now() - self.updated_at = datetime.now() - - def get_json(self) -> str: - """Convert user data to JSON string. - - Returns: - str: JSON string representation of the user data - """ - j: Dict[str, Any] = { - 'id': self.id, - 'name': self.name, - 'middle_name': self.middle_name, - 'last_name': self.last_name, - 'email': self.email, - 'password': self.password, - 'gender': self.gender, - 'age': self.age, - 'is_active': self.is_active, - 'created_at': self.created_at.isoformat(), - 'updated_at': self.updated_at.isoformat() - } - return json.dumps(j) - - -def generate_faker_users(count: int = DEFAULT_USER_COUNT) -> List[FakerUser]: - """Generate a list of fake users. - - Args: - count: Number of users to generate (default: DEFAULT_USER_COUNT) - - Returns: - List[FakerUser]: List of FakerUser objects - - Raises: - ValueError: If count is not a positive integer - """ - if not isinstance(count, int) or count <= 0: - raise ValueError("Count must be a positive integer") - return [FakerUser() for _ in range(count)] - - -def input_user_data(count: int, output_file: str = DEFAULT_OUTPUT_FILE) -> None: - """Generate fake users and save them to a JSON file. - - Args: - count: Number of users to generate - output_file: Path to the output JSON file (default: DEFAULT_OUTPUT_FILE) - - Raises: - ValueError: If count is not a positive integer - IOError: If there are issues writing to the file - json.JSONDecodeError: If there are issues with JSON encoding - """ - if not isinstance(count, int) or count <= 0: - raise ValueError("Count must be a positive integer") - - try: - users = generate_faker_users(count) - user_data = [json.loads(user.get_json()) for user in users] - - # Create directory only if the output file is in a subdirectory - output_path = Path(output_file) - output_path.parent.mkdir(parents=True, exist_ok=True) - - with output_path.open('w') as f: - json.dump(user_data, f, indent=2) - - print(f"Generated {count} users and saved to {output_file}") - except (IOError, json.JSONDecodeError) as e: - print(f"Error saving user data: {e}") - raise - except Exception as e: - print(f"Unexpected error: {e}") - raise - - -def main() -> None: - """Main function to generate fake users with user input.""" - try: - while True: - try: - count = int(input("Enter the number of users to generate (positive integer): ")) - if count <= 0: - print("Please enter a positive integer.") - continue - break - except ValueError: - print("Invalid input. Please enter a valid integer.") - - output_file = input("Enter the output filename (default: user_fake.json): ").strip() - if not output_file: - output_file = DEFAULT_OUTPUT_FILE - elif not output_file.endswith('.json'): - output_file += '.json' - - input_user_data(count, output_file) - except Exception as e: - print(f"Error in main: {e}") - raise - +from .cli import app if __name__ == '__main__': - main() \ No newline at end of file + app() \ No newline at end of file diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..723f478 --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1 @@ +"""Models package for Forger.""" \ No newline at end of file diff --git a/src/models/user.py b/src/models/user.py new file mode 100644 index 0000000..30be9d8 --- /dev/null +++ b/src/models/user.py @@ -0,0 +1,69 @@ +import json +from datetime import datetime +from faker import Faker +from typing import Dict, Any + +# Module-level constants +MIN_AGE = 18 +MAX_AGE = 99 +PASSWORD_LENGTH = 12 +GENDER_OPTIONS = ('male', 'female', 'other') + + +class FakerUser: + """A class to generate fake user data using the Faker library. + + This class generates random user data including personal information, + contact details, and account status. All data is generated using the + Faker library to ensure realistic and diverse test data. + + Attributes: + id (str): Unique identifier for the user + name (str): User's first name + middle_name (str): User's middle name + last_name (str): User's last name + email (str): User's email address + password (str): User's password + gender (str): User's gender + age (int): User's age + is_active (bool): User's account status + created_at (datetime): Account creation timestamp + updated_at (datetime): Last update timestamp + """ + + def __init__(self): + """Initialize a new FakerUser with random data.""" + fake = Faker() + self.id = fake.uuid4() + self.name = fake.first_name() + self.middle_name = fake.first_name() + self.last_name = fake.last_name() + email_base = f"{self.name.lower()}.{self.last_name.lower()}" + self.email = f"{email_base}@example.com" + self.password = fake.password(length=PASSWORD_LENGTH) + self.gender = fake.random_element(elements=GENDER_OPTIONS) + self.age = fake.random_int(min=MIN_AGE, max=MAX_AGE) + self.is_active = True + self.created_at = datetime.now() + self.updated_at = datetime.now() + + def get_json(self) -> str: + """Convert user data to JSON string. + + Returns: + str: JSON string representation of the user data + """ + j: Dict[str, Any] = { + 'id': self.id, + 'name': self.name, + 'middle_name': self.middle_name, + 'last_name': self.last_name, + 'email': self.email, + 'password': self.password, + 'gender': self.gender, + 'age': self.age, + 'is_active': self.is_active, + 'created_at': self.created_at.isoformat(), + 'updated_at': self.updated_at.isoformat() + } + return json.dumps(j) \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..6015795 --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1 @@ +"""Services package for Forger.""" \ No newline at end of file diff --git a/src/services/user_generator.py b/src/services/user_generator.py new file mode 100644 index 0000000..f565847 --- /dev/null +++ b/src/services/user_generator.py @@ -0,0 +1,57 @@ +from typing import List +from pathlib import Path +import json +from ..models.user import FakerUser + +# Module-level constants +DEFAULT_USER_COUNT = 10 +DEFAULT_OUTPUT_FILE = 'users.json' + + +def generate_faker_users(count: int = DEFAULT_USER_COUNT) -> List[FakerUser]: + """Generate a list of fake users. + + Args: + count: Number of users to generate (default: DEFAULT_USER_COUNT) + + Returns: + List[FakerUser]: List of FakerUser objects + + Raises: + ValueError: If count is not a positive integer + """ + if not isinstance(count, int) or count <= 0: + raise ValueError("Count must be a positive integer") + return [FakerUser() for _ in range(count)] + + +def save_users_to_file(count: int, output_file: str = DEFAULT_OUTPUT_FILE) -> None: + """Generate fake users and save them to a JSON file. + + Args: + count: Number of users to generate + output_file: Path to the output JSON file (default: DEFAULT_OUTPUT_FILE) + + Raises: + ValueError: If count is not a positive integer + IOError: If there are issues writing to the file + json.JSONDecodeError: If there are issues with JSON encoding + """ + if not isinstance(count, int) or count <= 0: + raise ValueError("Count must be a positive integer") + + try: + users = generate_faker_users(count) + user_data = [json.loads(user.get_json()) for user in users] + + # Create directory only if the output file is in a subdirectory + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + with output_path.open('w') as f: + json.dump(user_data, f, indent=2) + + except (IOError, json.JSONDecodeError) as e: + raise IOError(f"Error saving user data: {e}") + except Exception as e: + raise Exception(f"Unexpected error: {e}") \ No newline at end of file