Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,33 @@ on:
branches: [main, master]

jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install ruff
run: pip install ruff>=0.4.0

- name: Lint with ruff
run: ruff check ruf_common/

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ __pycache__/
.venv/
venv/
.vscode/
.coverage
build/
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pip install ruf-common
from ruf_common import *

# Or import specific modules
from ruf_common import data, helper, lfs
from ruf_common import data, helper, lfs # etc.
```

## Modules
Expand All @@ -45,3 +45,18 @@ The following modules are available:
## License

MIT

## Use of AI for Creating/Maintaining This Library

**No portion of this library was "vibe coded".**

Early versions of this library were written entirely without the use of AI tools.

Claude/Claude Code and GitHub Co-pilot have been used in a manner similar to pair-programming. This includes:
- improving alignment with "pythonic" best practices
- targeted code reviews
- resolving linter issues
- aiding in debugging and testing
- drafting individual functions/methods that I refine and test
- drafting portions of documentation
- drafting unit tests
4 changes: 3 additions & 1 deletion docs/PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ git tag vX.Y.Z
git push origin vX.Y.Z
```

### 4. Create the GitHub Release
### 4. Merge with Main

### 5. Create the GitHub Release

1. Go to [Releases → New release](https://github.com/brian-ruf/ruf-common-python/releases/new)
2. Select tag **`vX.Y.Z`**
Expand Down
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ruf-common"
version = "2.0.3"
version = "2.1.0"
description = "Functions common to several of Brian's Python projects."
requires-python = ">=3.9"
license = "MIT"
Expand Down Expand Up @@ -43,6 +43,7 @@ dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"pytest-asyncio>=0.21.0",
"ruff>=0.4.0",
]

[tool.setuptools.packages.find]
Expand All @@ -65,3 +66,10 @@ exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
]

[tool.ruff]
target-version = "py310"
line-length = 120

[tool.ruff.lint]
select = ["E9", "F"]
2 changes: 1 addition & 1 deletion ruf_common/country_code_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def country_name_to_code_api(country_name: str) -> str:
except Exception:
return country_name_to_code_fuzzy(country_name)

def demonstrate_conversion():
def demonstrate_conversion() -> None:
"""Demonstrate different country code conversion methods."""

test_countries = [
Expand Down
35 changes: 19 additions & 16 deletions ruf_common/data.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""
Functions for managing and manipulating XML, JSON and YAML content.
"""
from __future__ import annotations

import elementpath
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import tostring
from loguru import logger

from typing import Any, cast

# -------------------------------------------------------------------------
def detect_data_format(content):
def detect_data_format(content: str) -> str:
"""Detect whether the content is XML, JSON, or YAML based on its starting characters."""
content = content.lstrip() # Remove leading whitespace

Expand All @@ -25,7 +28,7 @@ def detect_data_format(content):

# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
def safe_load(content, data_format=""):
def safe_load(content: str, data_format: str = "") -> object | None:
"""Check if the provided content string is well-formed based on its format."""
data_object = None
if data_format == "":
Expand All @@ -43,7 +46,7 @@ def safe_load(content, data_format=""):

return data_object
# -------------------------------------------------------------------------
def safe_load_xml(content):
def safe_load_xml(content: str) -> ET.Element | None:
"""
Returns an XML tree if the provided XML string is well-formed.
If not well-formed, returns None.
Expand All @@ -64,7 +67,7 @@ def safe_load_xml(content):


# -------------------------------------------------------------------------
def safe_load_json(content):
def safe_load_json(content: str) -> dict | None:
"""
Returns a dict if the provided JSON string is well-formed.
If not well-formed, returns None.
Expand All @@ -82,7 +85,7 @@ def safe_load_json(content):
return data_object

# -------------------------------------------------------------------------
def safe_load_yaml(content):
def safe_load_yaml(content: str) -> dict | None:
"""
Returns a dict if the provided YAML string is well-formed.
If not well-formed, returns None.
Expand All @@ -100,7 +103,7 @@ def safe_load_yaml(content):
return data_object

# -------------------------------------------------------------------------
def xpath(tree, nsmap, xExpr, context=None):
def xpath(tree: Any, nsmap: dict, xExpr: str, context: ET.Element | None = None) -> object | None:
"""
Performs an xpath query either on the entire XML document
or on a context within the document.
Expand Down Expand Up @@ -143,7 +146,7 @@ def xpath(tree, nsmap, xExpr, context=None):

return result
# -------------------------------------------------------------------------
def xpath_atomic(tree, nsmap, xExpr, context=None):
def xpath_atomic(tree: Any, nsmap: dict, xExpr: str, context: ET.Element | None = None) -> str:
"""
Performs an xpath query either on the entire XML document
or on a context within the document.
Expand Down Expand Up @@ -182,7 +185,7 @@ def xpath_atomic(tree, nsmap, xExpr, context=None):
return str(ret_value)

# -------------------------------------------------------------------------
def remove_namespace(element):
def remove_namespace(element: ET.Element) -> None:
"""Remove namespace from an element and all its children"""
# Remove namespace from this element
if '}' in element.tag:
Expand All @@ -200,7 +203,7 @@ def remove_namespace(element):


# -------------------------------------------------------------------------
def get_markup_content(tree, nsmap, xExpr, context=None):
def get_markup_content(tree: Any, nsmap: dict, xExpr: str, context: ET.Element | None = None) -> str:
"""
Get the content of a specific XML element using XPath, preserving HTML formatting.

Expand All @@ -214,6 +217,7 @@ def get_markup_content(tree, nsmap, xExpr, context=None):
The content of the element as a string with HTML preserved, or empty string if not found
"""
ret_value = ""
element = None

try:
# First, try to get the entire element (not just its children)
Expand Down Expand Up @@ -244,7 +248,7 @@ def get_markup_content(tree, nsmap, xExpr, context=None):
# Now we have the element, let's extract its complete content
if hasattr(element, 'tag'):
# This is an Element object
ret_value = extract_element_content(element)
ret_value = extract_element_content(cast(ET.Element, element))
else:
# This might be a text node or something else
ret_value = str(element)
Expand All @@ -255,7 +259,7 @@ def get_markup_content(tree, nsmap, xExpr, context=None):
return ret_value

# -------------------------------------------------------------------------
def xml_to_string(element):
def xml_to_string(element: Any) -> str:
"""Convert an XML element or list of elements to a string."""
import copy
element_str = ""
Expand Down Expand Up @@ -283,7 +287,7 @@ def xml_to_string(element):


# -------------------------------------------------------------------------
def extract_element_content(element):
def extract_element_content(element: ET.Element | None) -> str:
"""
Extract the complete inner content of an XML element, preserving all HTML formatting
but removing namespaces. Handles both simple text content and complex mixed content.
Expand Down Expand Up @@ -345,7 +349,7 @@ def extract_element_content(element):


# -------------------------------------------------------------------------
def remove_namespace_from_html(html_str):
def remove_namespace_from_html(html_str: str) -> str:
"""
Remove XML namespace declarations from HTML string.

Expand All @@ -369,7 +373,7 @@ def remove_namespace_from_html(html_str):
return html_str

# -------------------------------------------------------------------------
def deserialize_xml(xml_string, nsmap):
def deserialize_xml(xml_string: str, nsmap: str) -> ET.Element | None:
"""Deserialize an XML string into a Python dictionary."""
ret_value = None
try:
Expand All @@ -385,8 +389,7 @@ def deserialize_xml(xml_string, nsmap):
return ret_value

# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
def get_attribute_value(element, attribute_name, default=""):
def get_attribute_value(element: ET.Element, attribute_name: str, default: str = "") -> str:
"""
Get the value of a specific attribute from an XML element.

Expand Down
Loading
Loading