Skip to content

Commit

Permalink
Merge pull request #82 from eli64s/refactor/readme-builder
Browse files Browse the repository at this point in the history
Refactor ReadmeBuilder to Class-Based Design and Enhance Configuration
  • Loading branch information
eli64s authored Jan 4, 2024
2 parents cf5c229 + 2e5c26c commit 67261d6
Show file tree
Hide file tree
Showing 22 changed files with 625 additions and 588 deletions.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "readmeai"
version = "0.4.0987"
version = "0.4.0988"
description = "🚀 Generate beautiful README.md files from the terminal using GPT LLM APIs 💫"
authors = ["Eli <0x.eli.64s@gmail.com>"]
license = "MIT"
Expand All @@ -16,11 +16,11 @@ keywords = [
"markdown",
"readme",
"ai",
"generator",
"devtools",
"documentation",
"documentation-generator",
"readme-md",
"automated-documentation",
"readme-generator",
"openai-api",
"automated-readme",
Expand Down
2 changes: 1 addition & 1 deletion readmeai/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import click
from click import Context, Parameter

from readmeai.config.settings import BadgeOptions, ImageOptions
from readmeai.config.enums import BadgeOptions, ImageOptions


def prompt_for_custom_image(
Expand Down
62 changes: 62 additions & 0 deletions readmeai/config/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Enum classes for the readmeai package."""

from enum import Enum


class GitService(str, Enum):
"""
Enum class for Git service details.
"""

LOCAL = "local"
GITHUB = "github.com"
GITLAB = "gitlab.com"
BITBUCKET = "bitbucket.org"

@property
def api_url(self):
"""Gets the API URL for the Git service."""
return {
GitService.LOCAL: None,
GitService.GITHUB: "https://api.github.com/repos/",
GitService.GITLAB: "https://api.gitlab.com/v4/projects/",
GitService.BITBUCKET: "https://api.bitbucket.org/2.0/repositories/",
}.get(self, None)

@property
def file_url_template(self):
"""Gets the file URL template for the Git service."""
return {
GitService.LOCAL: "{file_path}",
GitService.GITHUB: "https://github.com/{full_name}/blob/main/{file_path}",
GitService.GITLAB: "https://gitlab.com/{full_name}/-/blob/master/{file_path}",
GitService.BITBUCKET: "https://bitbucket.org/{full_name}/src/master/{file_path}",
}.get(self, None)


class BadgeOptions(str, Enum):
"""
Enum for CLI options for README file badge icons.
"""

DEFAULT = "default"
FLAT = "flat"
FLAT_SQUARE = "flat-square"
FOR_THE_BADGE = "for-the-badge"
PLASTIC = "plastic"
SKILLS = "skills"
SKILLS_LIGHT = "skills-light"
SOCIAL = "social"


class ImageOptions(str, Enum):
"""
Enum for CLI options for README file header images.
"""

CUSTOM = "CUSTOM"
DEFAULT = "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/ec559a9f6bfd399b82bb44393651661b08aaf7ba/icons/folder-markdown-open.svg"
BLACK = "https://img.icons8.com/external-tal-revivo-regular-tal-revivo/96/external-readme-is-a-easy-to-build-a-developer-hub-that-adapts-to-the-user-logo-regular-tal-revivo.png"
GREY = "https://img.icons8.com/external-tal-revivo-filled-tal-revivo/96/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-filled-tal-revivo.png"
PURPLE = "https://img.icons8.com/external-tal-revivo-duo-tal-revivo/100/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-duo-tal-revivo.png"
YELLOW = "https://img.icons8.com/pulsar-color/96/markdown.png"
224 changes: 113 additions & 111 deletions readmeai/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Data models and functions for configuring the readme-ai CLI tool."""

import os
from enum import Enum
import re
from importlib import resources
from pathlib import Path
from typing import Dict, List, Optional, Union
Expand All @@ -10,107 +9,17 @@
import pkg_resources
from pydantic import BaseModel, validator

from readmeai.config.enums import GitService
from readmeai.core.factory import FileHandler
from readmeai.core.logger import Logger

logger = Logger(__name__)


class GitService(str, Enum):
"""
Enum class for Git service details.
"""
class GitSettingsValidator:
"""Validator class for GitSettings."""

LOCAL = ("local", None, "{file_path}")
GITHUB = (
"github.com",
"https://api.github.com/repos/",
"https://github.com/{full_name}/blob/main/{file_path}",
)
GITLAB = (
"gitlab.com",
"https://api.gitlab.com/v4/projects/",
"https://gitlab.com/{full_name}/-/blob/master/{file_path}",
)
BITBUCKET = (
"bitbucket.org",
"https://api.bitbucket.org/2.0/repositories/",
"https://bitbucket.org/{full_name}/src/master/{file_path}",
)

def __new__(cls, host, api_url, file_url) -> object:
"""Create a new instance of the GitService enum."""
obj = str.__new__(cls, host)
obj._value_ = host
obj.host = host
obj.api_url = api_url
obj.file_url = file_url
return obj

def extract_name_from_host(repo_host: str) -> str:
"""Return the hostname without periods."""
if repo_host == GitService.LOCAL.host:
return GitService.LOCAL.host
else:
return repo_host.split(".")[0]


class BadgeOptions(str, Enum):
"""
Enum for CLI options for README file badge icons.
"""

DEFAULT = "default"
FLAT = "flat"
FLAT_SQUARE = "flat-square"
FOR_THE_BADGE = "for-the-badge"
PLASTIC = "plastic"
SKILLS = "skills"
SKILLS_LIGHT = "skills-light"
SOCIAL = "social"


class ImageOptions(str, Enum):
"""
Enum for CLI options for README file header images.
"""

CUSTOM = "CUSTOM"
DEFAULT = "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/ec559a9f6bfd399b82bb44393651661b08aaf7ba/icons/folder-markdown-open.svg"
BLACK = "https://img.icons8.com/external-tal-revivo-regular-tal-revivo/96/external-readme-is-a-easy-to-build-a-developer-hub-that-adapts-to-the-user-logo-regular-tal-revivo.png"
GREY = "https://img.icons8.com/external-tal-revivo-filled-tal-revivo/96/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-filled-tal-revivo.png"
PURPLE = "https://img.icons8.com/external-tal-revivo-duo-tal-revivo/100/external-markdown-a-lightweight-markup-language-with-plain-text-formatting-syntax-logo-duo-tal-revivo.png"
YELLOW = "https://img.icons8.com/pulsar-color/96/markdown.png"


class CliSettings(BaseModel):
"""CLI options for the readme-ai application."""

emojis: bool = True
offline: bool = False


class FileSettings(BaseModel):
"""Pydantic model for configuration file paths."""

dependency_files: str
identifiers: str
ignore_files: str
language_names: str
language_setup: str
output: str
shields_icons: str
skill_icons: str


class GitSettings(BaseModel):
"""Codebase repository settings and validations."""

repository: Union[str, Path]
source: Optional[str]
name: Optional[str]

@validator("repository", pre=True, always=True)
@classmethod
def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]:
"""Validate the repository URL or path."""
if isinstance(value, str):
Expand All @@ -120,7 +29,7 @@ def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]:
try:
parsed_url = urlparse(value)
if parsed_url.scheme in ["http", "https"] and any(
service.host in parsed_url.netloc for service in GitService
service in parsed_url.netloc for service in GitService
):
return value
except ValueError:
Expand All @@ -129,22 +38,23 @@ def validate_repository(cls, value: Union[str, Path]) -> Union[str, Path]:
return value
raise ValueError(f"Invalid repository URL or path: {value}")

@validator("source", pre=True, always=True)
def set_source(cls, value: Optional[str], values: dict) -> str:
"""Sets the Git service source from the repository provided."""
@classmethod
def set_host(cls, value: Optional[str], values: dict) -> str:
"""Sets the Git service host from the repository provided."""
repo = values.get("repository")
if isinstance(repo, Path) or (
isinstance(repo, str) and Path(repo).is_dir()
):
return GitService.LOCAL.host
return GitService.LOCAL

parsed_url = urlparse(str(repo))
for service in GitService:
if service.host in parsed_url.netloc:
return service.host
return GitService.LOCAL.host
if service in parsed_url.netloc:
return service.split(".")[0]

return GitService.LOCAL

@validator("name", pre=True, always=True)
@classmethod
def set_name(cls, value: Optional[str], values: dict) -> str:
"""Sets the repository name from the repository provided."""
repo = values.get("repository")
Expand All @@ -156,6 +66,91 @@ def set_name(cls, value: Optional[str], values: dict) -> str:
return name.removesuffix(".git")
return "n/a"

@classmethod
def set_source(cls, value: Optional[str], values: dict) -> str:
repo = values.get("repository")
if isinstance(repo, Path) or (
isinstance(repo, str) and Path(repo).is_dir()
):
return GitService.LOCAL

parsed_url = urlparse(str(repo))
for service in GitService:
if service in parsed_url.netloc:
return service
return GitService.LOCAL

@classmethod
def validate_full_name(cls, value: Optional[str], values: dict) -> str:
"""Validator for getting the full name of the repository."""
url_or_path = values.get("repository")

path = (
url_or_path if isinstance(url_or_path, Path) else Path(url_or_path)
)
if path.exists():
return str(path.name)

patterns = {
GitService.GITHUB: r"https?://github.com/([^/]+)/([^/]+)",
GitService.GITLAB: r"https?://gitlab.com/([^/]+)/([^/]+)",
GitService.BITBUCKET: r"https?://bitbucket.org/([^/]+)/([^/]+)",
}

for _, pattern in patterns.items():
match = re.match(pattern, url_or_path)
if match:
user_name, repo_name = match.groups()
return f"{user_name}/{repo_name}"

raise ValueError("Error: invalid repository URL or path.")


class CliSettings(BaseModel):
"""CLI options for the readme-ai application."""

emojis: bool
offline: bool


class FileSettings(BaseModel):
"""File paths for the readme-ai application."""

dependency_files: str
identifiers: str
ignore_files: str
language_names: str
language_setup: str
output: str
shields_icons: str
skill_icons: str


class GitSettings(BaseModel):
"""Codebase repository settings and validations."""

repository: Union[str, Path]
full_name: Optional[str]
host: Optional[str]
name: Optional[str]
source: Optional[str]

_validate_repository = validator("repository", pre=True, always=True)(
GitSettingsValidator.validate_repository
)
_validate_full_name = validator("full_name", pre=True, always=True)(
GitSettingsValidator.validate_full_name
)
_set_host = validator("host", pre=True, always=True)(
GitSettingsValidator.set_host
)
_set_name = validator("name", pre=True, always=True)(
GitSettingsValidator.set_name
)
_set_source = validator("source", pre=True, always=True)(
GitSettingsValidator.set_source
)


class LlmApiSettings(BaseModel):
"""Pydantic model for OpenAI LLM API details."""
Expand Down Expand Up @@ -270,21 +265,28 @@ def _get_config_dict(handler: FileHandler, file_path: str) -> dict:
"""Get configuration dictionary from TOML file."""
try:
resource_path = resources.files("readmeai.settings") / file_path
logger.info(f"Resource path using importlib: {resource_path}")
logger.info(f"Using importlib.resources to load: {resource_path}")

except TypeError as exc_info:
logger.debug(f"Error with importlib.resources: {exc_info}")
logger.debug(f"Error using importlib.resources: {exc_info}")

try:
resource_path = Path(
pkg_resources.resource_filename(
"readmeai", f"settings/{file_path}"
)
).resolve()
logger.info(f"Resource path using pkg_resources: {resource_path}")
logger.info(
f"Using pkg_resources.resource_filename: {resource_path}"
)

except FileNotFoundError as exc_info:
logger.debug(f"Error with pkg_resources: {exc_info}")
raise
logger.debug(
f"Error using pkg_resources.resource_filename: {exc_info}"
)
raise FileNotFoundError(f"Config file not found: {file_path}")

if not os.path.exists(resource_path):
if not resource_path.exists():
raise FileNotFoundError(f"Config file not found: {resource_path}")

return handler.read(resource_path)
Expand Down
Loading

0 comments on commit 67261d6

Please sign in to comment.