feat: Initialize hostsd project with PyPI CI/CD

Introduced a comprehensive setup for `hostsd`, a simple hosts file manager, marking the project's inception. This entails crafting the PyPI CI/CD pipeline configuration, ensuring seamless publication processes on tag-based releases. Additionally, standard project necessities such as `.gitignore`, `LICENSE`, and `README.md` were put in place, alongside the core project configuration in `pyproject.toml`. The initial source code foundation is laid out in `src/hostsd`, featuring argument parsing and file management logic essential for hosts file manipulation.

The PyPI CI/CD configuration in `.forgejo/workflows/pypi.yml` facilitates automated publishing to PyPI upon tagging, backed by Docker to guarantee environment consistency. The project adopts a community-friendly stance with an MIT license, encouraging open collaboration. Documentation is immediately available via `README.md`, covering installation, usage, and automated update setups, ensuring users can effectively leverage `hostsd` from the get-go.

This setup underscores the project's commitment to enabling efficient hosts file management across various environments, laying the groundwork for future enhancements and community contributions.
This commit is contained in:
Kumi 2024-05-02 14:49:06 +02:00
commit 5f429d73ea
Signed by: kumi
GPG key ID: ECBCC9082395383F
7 changed files with 291 additions and 0 deletions

View file

@ -0,0 +1,32 @@
name: Python Package CI/CD
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
publish:
name: Publish to PyPI
container:
image: node:20-bookworm
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt update
apt install -y python3 python3-venv
- name: Publish to PyPI
run: |
python3 -m venv venv
. ./venv/bin/activate
pip install -U hatchling twine build
python -m build .
python -m twine upload --username __token__ --password ${PYPI_TOKEN} dist/*
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
build/
dist/
venv/
*.pyc
__pycache__/

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2023 Kumi Mitterer <hostsd@kumi.email>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

82
README.md Normal file
View file

@ -0,0 +1,82 @@
# hostsd - A simple hosts file manager
[![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-support%20us!-pink?logo=coffeescript)](https://private.coffee)
[![PyPI](https://shields.private.coffee/pypi/v/hostsd)](https://pypi.org/project/hostsd/)
[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/hostsd)](https://pypi.org/project/hostsd/)
[![PyPI - License](https://shields.private.coffee/pypi/l/hostsd)](https://pypi.org/project/hostsd/)
[![Git Workflow Status](https://shields.private.coffee/gitea/last-commit/kumi/hostsd?gitea-url=https://git.private.coffee)](https://git.private.coffee/kumi/hostsd)
`hostsd` is a simple hosts file manager that allows you to separate your hosts file into multiple files and easily enable or disable them. It's useful for development environments where you need to manage lots of hosts entries, or for managing ad-blocking hosts files.
## Dependencies
- Python 3.8 or later (earlier versions may work but are untested)
- Linux or macOS (should work on Windows too but is untested)
## Installation
```bash
pip install hostsd
```
## Usage
To write the contents of `/etc/hosts.d/*` to `/etc/hosts`:
```bash
sudo hostsd
```
You can also specify the input and output paths:
```bash
hostsd -i /etc/hosts.d -o /etc/hosts
```
You can disable a file by adding a `.disabled` extension:
```bash
mv /etc/hosts.d/10-my-file /etc/hosts.d/10-my-file.disabled
```
Hidden files (files starting with a dot) are ignored as well.
## Running hostsd automatically
You can run `hostsd` automatically using a cron job or a systemd timer. Here's an example of a systemd timer (assuming you have installed `hostsd` globally, for simplicity):
```ini
# /etc/systemd/system/hostsd.timer
[Unit]
Description=Update hosts file every minute
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target
```
```ini
# /etc/systemd/system/hostsd.service
[Unit]
Description=Update hosts file
[Service]
Type=oneshot
ExecStart=/usr/bin/hostsd
```
```bash
sudo systemctl enable hostsd.timer
sudo systemctl start hostsd.timer
```
This will run `hostsd` every minute. So you can just drop a new file in `/etc/hosts.d` and it will be picked up automatically.
You could even combine this with a git repository and you have a simple way to manage your hosts files across multiple machines or share them with others, without needing to set up and manage a full DNS server.
## License
hostsd is licensed under the MIT license. See [LICENSE](LICENSE) for the full license text.

27
pyproject.toml Normal file
View file

@ -0,0 +1,27 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "hostsd"
version = "0.1.0"
authors = [
{ name="Kumi Mitterer", email="hostsd@kumi.email" },
]
description = "A simple hosts file manager"
readme = "README.md"
license = { file="LICENSE" }
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.scripts]
hostsd = "hostsd.__main__:main"
[project.urls]
"Homepage" = "https://git.private.coffee/kumi/hostsd"
"Bug Tracker" = "https://git.private.coffee/kumi/hostsd/issues"
"Source Code" = "https://git.private.coffee/kumi/hostsd"

0
src/hostsd/__init__.py Normal file
View file

126
src/hostsd/__main__.py Normal file
View file

@ -0,0 +1,126 @@
from pathlib import Path
from os import PathLike
from typing import Union
from argparse import ArgumentParser
from pathlib import Path
import sys
HOSTS_DIR = "/etc/hosts.d/"
HOSTS_FILE = "/etc/hosts"
HOSTS_DIR_WINDOWS = "C:\\Windows\\System32\\drivers\\etc\\hosts.d\\"
HOSTS_FILE_WINDOWS = "C:\\Windows\\System32\\drivers\\etc\\hosts"
def get_new_content(dirpath: Union[str, PathLike] = HOSTS_DIR) -> str:
"""Read all files from a directory and join their contents in a string
This will not read files with names that begin with a period ("."), or binary files.
Args:
dir (str, optional): The directory from which to read files. Defaults to "/etc/hosts.d/".
Raises:
IOError: Raised if the provided source path does not exist or is not a directory.
Returns:
str: Joined content of read files
"""
content: str = ""
directory = Path(dirpath)
if not (directory.exists() and directory.is_dir()):
raise IOError(f"Directory {dirpath} does not exist or is not a directory.")
for infile in sorted(list(directory.iterdir()), key=lambda f: f.name):
if (
infile.is_file()
and not infile.name.startswith(".")
and not infile.name.endswith(".disabled")
):
with infile.open('r') as openfile:
try:
filecontent = openfile.read()
except UnicodeDecodeError:
print(f"File {infile.name} is not a text file - skipping")
if content:
content += "\n\n"
content += f"# {infile.name}\n\n{filecontent}"
return content
def write_hosts_file(content: str, path: Union[str, PathLike] = HOSTS_FILE):
"""Simple function writing text content to a file given by path
Args:
content (str): Text content to be written.
path (Union[str, PathLike], optional): Path of the file to write to. Defaults to "/etc/hosts".
Raises:
IOError: Raised if the provided file path could not be written to.
"""
with Path(path).open('w') as hostsfile:
try:
hostsfile.write(content)
except Exception as e:
raise IOError(f"Unable to write file {path} {e}")
def main():
parser = ArgumentParser(
description="Update /etc/hosts file with contents of /etc/hosts.d/ directory"
)
parser.add_argument(
"-d",
"--dir",
help=f"Directory containing the hosts files to be merged (default: {HOSTS_DIR})",
default=HOSTS_DIR,
)
parser.add_argument(
"-o",
"--output",
help=f"File to write the merged content to (default: {HOSTS_FILE})",
default=HOSTS_FILE,
)
args = parser.parse_args()
if args.dir:
input_dir = Path(args.dir)
else:
if sys.platform == "win32":
input_dir = Path(HOSTS_DIR_WINDOWS)
else:
input_dir = Path(HOSTS_DIR)
if not input_dir.exists():
print(f"Directory {args.dir} does not exist exiting")
return
if args.output:
output_file = Path(args.output)
else:
if sys.platform == "win32":
output_file = Path(HOSTS_FILE_WINDOWS)
else:
output_file = Path(HOSTS_FILE)
if not output_file.exists():
print(f"File {args.output} does not exist exiting")
return
try:
write_hosts_file(get_new_content(args.dir), args.output)
except IOError as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()