Skip to main content

Universal auto-number generator library with flexible templates and multi-database support

Project description

📦 Risnaberti Nanumber

PyPI version Python version License Status

Universal auto-number generator library by Risna Berti
Aman, fleksibel, dan mudah diintegrasikan ke FastAPI, Django, Flask, atau framework lainnya.


✨ Fitur Utama

Format fleksibel dengan placeholder yang mudah
Auto-reset per tahun otomatis
Thread-safe & Process-safe - aman dari race condition
Multi-database support - SQLite, PostgreSQL, MySQL
Production-ready - siap pakai untuk aplikasi skala besar
Zero configuration - langsung pakai tanpa setup rumit


🚀 Instalasi

pip install risnaberti-nanumber

Untuk PostgreSQL:

pip install risnaberti-nanumber psycopg2-binary

Untuk MySQL:

pip install risnaberti-nanumber pymysql

🎯 Quick Start

Basic Usage

from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

# Setup storage (SQLite untuk demo)
storage = SQLAlchemyStorage("sqlite:///nanumber.db")
gen = NumberGenerator(storage)

# Generate number dengan template
code = gen.generate(
    key="supplier",
    template="SUP-{y}{number}",
    pad=4
)
print(code)  # SUP-250001

Using Templates (Recommended)

from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

storage = SQLAlchemyStorage("sqlite:///nanumber.db")

# Define templates sekali di awal
gen = NumberGenerator(
    storage=storage,
    templates={
        "supplier": {
            "template": "SUP-{y}{number}",
            "pad": 4,
        },
        "invoice": {
            "template": "INV-{Y}-{number}",
            "pad": 5,
        },
        "purchase": {
            "template": "PO-{y}{m}{number}",
            "pad": 3,
        }
    }
)

# Sekarang tinggal panggil dengan key
print(gen.generate("supplier"))   # SUP-250001
print(gen.generate("invoice"))    # INV-2025-00001
print(gen.generate("purchase"))   # PO-2511001

📋 Format Placeholders

Placeholder Output Deskripsi
{Y} 2025 Tahun 4 digit
{y} 25 Tahun 2 digit
{m} 01-12 Bulan 2 digit
{d} 01-31 Tanggal 2 digit
{H} 00-23 Jam 2 digit
{M} 00-59 Menit 2 digit
{S} 00-59 Detik 2 digit
{number} 0001 Nomor urut auto-increment

Contoh Format

# Tahun 2 digit + nomor
"SUP-{y}{number}"   SUP-250001

# Tahun penuh + separator + nomor
"INV-{Y}-{number}"   INV-2025-00001

# Dengan tanggal
"TRX-{Y}{m}{d}-{number}"   TRX-20251113-0001

# Custom prefix
"CUST-{y}-{number}"   CUST-25-00001

PENTING: Padding diatur via parameter pad, bukan di template!

# ❌ SALAH - Ini akan error
gen.generate("invoice", "INV-{number:05d}")

# ✅ BENAR - Gunakan parameter pad
gen.generate("invoice", "INV-{number}", pad=5)

💾 Storage Options

1️⃣ MemoryStorage (Development/Testing)

from risnaberti.nanumber import NumberGenerator, MemoryStorage

storage = MemoryStorage()
gen = NumberGenerator(storage)

code = gen.generate("test", "TEST-{number}", pad=4)
print(code)  # TEST-0001

Karakteristik:

  • ✅ Cepat, tidak butuh database
  • ❌ Data hilang saat restart
  • 🎯 Use case: Testing, demo, unit tests

2️⃣ SQLAlchemyStorage (Production)

SQLite (Simple projects)

from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

storage = SQLAlchemyStorage("sqlite:///nanumber.db")
gen = NumberGenerator(storage)

Karakteristik:

  • ✅ File-based, mudah setup
  • ⚠️ Concurrency terbatas (single process recommended)
  • 🎯 Use case: Small apps, prototypes, single-server

PostgreSQL (Production recommended)

storage = SQLAlchemyStorage(
    "postgresql://user:password@localhost:5432/mydb"
)
gen = NumberGenerator(storage)

Karakteristik:

  • ✅ Excellent concurrency (row-level locking)
  • ✅ Multi-process safe (Gunicorn, Celery, etc)
  • ✅ Production-grade reliability
  • 🎯 Use case: Production apps, microservices, high-traffic

MySQL/MariaDB

storage = SQLAlchemyStorage(
    "mysql+pymysql://user:password@localhost:3306/mydb"
)
gen = NumberGenerator(storage)

Karakteristik:

  • ✅ Good concurrency support
  • ✅ Wide compatibility
  • 🎯 Use case: Production apps, existing MySQL infrastructure

🔧 Integrasi dengan Framework

FastAPI Integration

# app/main.py
from fastapi import FastAPI, Depends
from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

app = FastAPI()

# Initialize once (singleton pattern)
_storage = None
_generator = None

def get_generator() -> NumberGenerator:
    global _storage, _generator
    if _generator is None:
        _storage = SQLAlchemyStorage("sqlite:///nanumber.db")
        _generator = NumberGenerator(
            storage=_storage,
            templates={
                "supplier": {"template": "SUP-{y}{number}", "pad": 4},
                "invoice": {"template": "INV-{Y}-{number}", "pad": 5},
            }
        )
    return _generator

@app.post("/suppliers")
async def create_supplier(gen: NumberGenerator = Depends(get_generator)):
    supplier_code = gen.generate("supplier")
    return {"code": supplier_code}

@app.post("/invoices")
async def create_invoice(gen: NumberGenerator = Depends(get_generator)):
    invoice_number = gen.generate("invoice")
    return {"number": invoice_number}

@app.get("/generate/{entity}")
async def generate_code(
    entity: str, 
    gen: NumberGenerator = Depends(get_generator)
):
    try:
        code = gen.generate(entity)
        return {"entity": entity, "code": code}
    except Exception as e:
        return {"error": str(e)}, 400

Test endpoints:

curl http://localhost:8000/generate/supplier
# {"entity": "supplier", "code": "SUP-250001"}

curl http://localhost:8000/generate/invoice
# {"entity": "invoice", "code": "INV-2025-00001"}

Django Integration

Step 1: Setup di settings.py

# myproject/settings.py
from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

# Initialize Nanumber (singleton)
NANUMBER_STORAGE = SQLAlchemyStorage(
    f"postgresql://{DATABASES['default']['USER']}:"
    f"{DATABASES['default']['PASSWORD']}@"
    f"{DATABASES['default']['HOST']}/"
    f"{DATABASES['default']['NAME']}"
)

NANUMBER_GENERATOR = NumberGenerator(
    storage=NANUMBER_STORAGE,
    templates={
        "supplier": {"template": "SUP-{y}{number}", "pad": 4},
        "invoice": {"template": "INV-{Y}-{number}", "pad": 5},
        "customer": {"template": "CUST-{y}{number}", "pad": 4},
    }
)

Step 2: Gunakan di Models/Signals

# apps/supplier/models.py
from django.db import models

class Supplier(models.Model):
    code = models.CharField(max_length=50, unique=True, blank=True)
    name = models.CharField(max_length=255)
    # ... other fields

# apps/supplier/signals.py
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.conf import settings
from .models import Supplier

@receiver(pre_save, sender=Supplier)
def generate_supplier_code(sender, instance, **kwargs):
    if not instance.code:  # Only generate if empty
        instance.code = settings.NANUMBER_GENERATOR.generate("supplier")

# apps/supplier/apps.py
from django.apps import AppConfig

class SupplierConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.supplier'

    def ready(self):
        import apps.supplier.signals  # Register signals

Flask Integration

# app.py
from flask import Flask, jsonify
from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

app = Flask(__name__)

# Initialize Nanumber
storage = SQLAlchemyStorage("sqlite:///nanumber.db")
gen = NumberGenerator(
    storage=storage,
    templates={
        "supplier": {"template": "SUP-{y}{number}", "pad": 4},
        "invoice": {"template": "INV-{Y}-{number}", "pad": 5},
    }
)

@app.route("/generate/<entity>")
def generate_number(entity):
    try:
        code = gen.generate(entity)
        return jsonify({"entity": entity, "code": code})
    except Exception as e:
        return jsonify({"error": str(e)}), 400

if __name__ == "__main__":
    app.run(debug=True)

🧪 Advanced Usage

Auto-reset per Tahun

from datetime import datetime

# Generate untuk tahun 2024
code_2024 = gen.generate(
    "invoice", 
    "INV-{Y}-{number}",
    pad=5,
    date=datetime(2024, 12, 31)
)
print(code_2024)  # INV-2024-00001

# Generate untuk tahun 2025 - otomatis reset ke 1
code_2025 = gen.generate(
    "invoice",
    "INV-{Y}-{number}",
    pad=5,
    date=datetime(2025, 1, 1)
)
print(code_2025)  # INV-2025-00001

Catatan: Reset otomatis dilakukan oleh SQLAlchemyStorage berdasarkan kolom last_reset_year di database.


Manual Reset

# Reset nomor ke 0 (next generate akan jadi 1)
gen.reset("supplier", 0)

# Reset ke nilai tertentu
gen.reset("invoice", 100)  # Next generate akan jadi 101

# Get last number
last_num = gen.storage.get_last_number("supplier")
print(f"Last number: {last_num}")

Custom Padding

# Default padding (4 digit)
gen.generate("test", "T-{number}")  # T-0001

# Custom padding (6 digit)
gen.generate("test", "T-{number}", pad=6)  # T-000001

# No padding (1 digit minimal)
gen.generate("test", "T-{number}", pad=1)  # T-1

# Padding dengan karakter lain
gen.generate("test", "T-{number}", pad=4, pad_char="X")  # T-XXX1

# Padding kanan
gen.generate("test", "T-{number}", pad=4, pad_side="right")  # T-1000

🧠 Thread Safety Test

import threading
from risnaberti.nanumber import NumberGenerator, SQLAlchemyStorage

storage = SQLAlchemyStorage("sqlite:///test.db")
gen = NumberGenerator(storage)

results = []

def worker():
    code = gen.generate("test", "T-{number}", pad=4)
    results.append(code)
    print(code)

# Spawn 100 threads
threads = [threading.Thread(target=worker) for _ in range(100)]
for t in threads:
    t.start()
for t in threads:
    t.join()

# Check uniqueness
print(f"Generated: {len(results)}")
print(f"Unique: {len(set(results))}")
assert len(results) == len(set(results)), "Duplikasi terdeteksi!"

Output:

T-0001
T-0002
T-0003
...
T-0100
Generated: 100
Unique: 100

Semua nomor unik - tidak ada duplikasi! ✅


📊 Database Schema

Nanumber otomatis membuat tabel auto_numbers:

CREATE TABLE auto_numbers (
    key VARCHAR(100) PRIMARY KEY,
    last_value INTEGER NOT NULL DEFAULT 0,
    last_reset_year INTEGER NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE
);

Contoh data:

key last_value last_reset_year updated_at
supplier 42 2025 2025-11-13 10:30:00
invoice 158 2025 2025-11-13 11:15:00

🎨 Real-world Examples

E-commerce System

templates = {
    "customer": {
        "template": "CUST-{y}{number}",
        "pad": 6
    },
    "order": {
        "template": "ORD-{Y}{m}{d}-{number}",
        "pad": 4
    },
    "invoice": {
        "template": "INV-{Y}-{number}",
        "pad": 8
    },
    "shipment": {
        "template": "SHIP-{y}{m}{number}",
        "pad": 5
    }
}

gen = NumberGenerator(storage, templates=templates)

# Generate codes
customer_code = gen.generate("customer")     # CUST-25000001
order_number = gen.generate("order")         # ORD-20251113-0001
invoice_number = gen.generate("invoice")     # INV-2025-00000001
shipment_code = gen.generate("shipment")     # SHIP-251100001

Manufacturing System

templates = {
    "production_order": {
        "template": "PO-{Y}-{number}",
        "pad": 5
    },
    "batch": {
        "template": "BATCH-{y}{m}{d}{H}{number}",
        "pad": 3
    },
    "quality_check": {
        "template": "QC-{y}{number}",
        "pad": 6
    }
}

gen = NumberGenerator(storage, templates=templates)

po_number = gen.generate("production_order")  # PO-2025-00001
batch_code = gen.generate("batch")            # BATCH-25111310001
qc_number = gen.generate("quality_check")     # QC-25000001

🐛 Troubleshooting

Error: TemplateNotFoundError

# ❌ Error
gen.generate("unknown_key")
# TemplateNotFoundError: Template 'unknown_key' not found

# ✅ Solution 1: Tambahkan template
gen.templates["unknown_key"] = {
    "template": "UK-{number}",
    "pad": 4
}

# ✅ Solution 2: Provide template inline
gen.generate("unknown_key", "UK-{number}", pad=4)

Error: TemplateError: Unknown placeholder

# ❌ Error
gen.generate("test", "T-{number:04d}")
# TemplateError: Unknown placeholder: {number:04d}

# ✅ Solution: Gunakan parameter pad
gen.generate("test", "T-{number}", pad=4)

Error: Database locked (SQLite)

# ❌ Problem: SQLite tidak handle concurrent writes dengan baik

# ✅ Solution: Gunakan PostgreSQL atau MySQL
storage = SQLAlchemyStorage(
    "postgresql://user:pass@localhost/mydb"
)

📚 API Reference

NumberGenerator

__init__(storage, default_pad=4, default_pad_char="0", default_pad_side="left", templates=None)

Initialize number generator.

Parameters:

  • storage: Storage backend (MemoryStorage or SQLAlchemyStorage)
  • default_pad: Default padding length (default: 4)
  • default_pad_char: Padding character (default: "0")
  • default_pad_side: Padding side "left" or "right" (default: "left")
  • templates: Dict of predefined templates

generate(key, template=None, pad=None, pad_char=None, pad_side=None, date=None)

Generate auto-number for given key.

Parameters:

  • key: Unique identifier for sequence
  • template: Format template string
  • pad: Number padding length
  • pad_char: Character for padding
  • pad_side: "left" or "right"
  • date: Date for placeholders (default: now)

Returns: Generated number string

Raises:

  • TemplateNotFoundError: If key not in templates and template not provided
  • TemplateError: If template contains invalid placeholders

reset(key, value=0)

Reset number sequence.

Parameters:

  • key: Unique identifier for sequence
  • value: Reset to this value (default: 0)

SQLAlchemyStorage

__init__(db_url="sqlite:///nanumber.db")

Initialize storage with database URL.

Parameters:

  • db_url: SQLAlchemy database URL

Examples:

# SQLite
SQLAlchemyStorage("sqlite:///nanumber.db")

# PostgreSQL
SQLAlchemyStorage("postgresql://user:pass@localhost/db")

# MySQL
SQLAlchemyStorage("mysql+pymysql://user:pass@localhost/db")

🤝 Contributing

Contributions are welcome! Silakan buat Pull Request atau Issue di GitHub.

Development Setup

# Clone repository
git clone https://github.com/risnaberti/nanumber.git
cd nanumber

# Install dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v --cov=risnaberti.nanumber

📄 License

MIT © 2025 Risna Berti


💬 Support & Contact

Ada pertanyaan atau butuh bantuan?

📧 Email: risnaberti07@gmail.com
🐙 GitHub: @risnaberti
💼 LinkedIn: Risna Berti


🌟 Show Your Support

Jika library ini bermanfaat, kasih ⭐ di GitHub ya!

👉 https://github.com/risnaberti/nanumber


Made with ❤️ by Risna Berti (Nana)

"Because numbering should be smart, safe, and beautiful."

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

risnaberti_nanumber-0.2.0.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

risnaberti_nanumber-0.2.0-py3-none-any.whl (12.5 kB view details)

Uploaded Python 3

File details

Details for the file risnaberti_nanumber-0.2.0.tar.gz.

File metadata

  • Download URL: risnaberti_nanumber-0.2.0.tar.gz
  • Upload date:
  • Size: 18.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for risnaberti_nanumber-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6c10c5a2a82c9d1fc130e04da996cf2db7b8ae9a3ddcb1142b70674cd4cb6548
MD5 fdcde8e967d790f0744e0b39f11d54e7
BLAKE2b-256 4eff97ae46353e9dd7f87eed10a0f5fcf93332c18335142aa9fde205f5a2ac5c

See more details on using hashes here.

File details

Details for the file risnaberti_nanumber-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for risnaberti_nanumber-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 33f867c0ba7ee6c7e3156ae0fe216e0503ac9ff615e2c822a2b41b99b412a171
MD5 b6fec5a8d9699648bb071b0f77a53d7b
BLAKE2b-256 64c253164e6c00e4960693218cbbfa767a4ba85cc5ba418fd98c888e6de9bd37

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page