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

nanumber_py-0.2.0.tar.gz (15.5 kB view details)

Uploaded Source

Built Distribution

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

nanumber_py-0.2.0-py3-none-any.whl (12.4 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for nanumber_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 293348c13471ccbb42ff6ec265492148649e8c9c3ee5757b58dc8497508e5419
MD5 d97acf776868b98d885341e5c51829fb
BLAKE2b-256 e43e54136f5186f2ed1f8048e17c2bcfd0e582c8b916fcd31b8570c397851d44

See more details on using hashes here.

File details

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

File metadata

  • Download URL: nanumber_py-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 12.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for nanumber_py-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 95476b31851d92775c9b22d1aa0f9e286fbd0cde7da538182cd3418d257b2e84
MD5 16215046b484395bf543df819880e64c
BLAKE2b-256 3963ae27bd9addcae72a196411e902b79610c7324ba2f8a03d8d605df1a495c7

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