nkscoder-courier 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- courier/__init__.py +19 -0
- courier/admin.py +44 -0
- courier/apps.py +7 -0
- courier/calculator.py +136 -0
- courier/conf.py +60 -0
- courier/migrations/0001_initial.py +174 -0
- courier/migrations/__init__.py +0 -0
- courier/models.py +96 -0
- courier/test_settings.py +12 -0
- courier/tests.py +46 -0
- courier/urls.py +7 -0
- courier/views.py +28 -0
- nkscoder_courier-1.1.1.dist-info/METADATA +272 -0
- nkscoder_courier-1.1.1.dist-info/RECORD +17 -0
- nkscoder_courier-1.1.1.dist-info/WHEEL +5 -0
- nkscoder_courier-1.1.1.dist-info/licenses/LICENSE +21 -0
- nkscoder_courier-1.1.1.dist-info/top_level.txt +1 -0
courier/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Django zone-based courier shipping calculator by Nitesh Kumar Singh (nkscoder)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.1.1"
|
|
4
|
+
__author__ = "Nitesh Kumar Singh"
|
|
5
|
+
__author_handle__ = "nkscoder"
|
|
6
|
+
|
|
7
|
+
__all__ = ["__version__", "calculate_courier_cost", "courier_cost", "CourierCalculationError"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def __getattr__(name):
|
|
11
|
+
if name in {"calculate_courier_cost", "courier_cost", "CourierCalculationError"}:
|
|
12
|
+
from .calculator import (
|
|
13
|
+
CourierCalculationError,
|
|
14
|
+
calculate_courier_cost,
|
|
15
|
+
courier_cost,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return locals()[name]
|
|
19
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
courier/admin.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from .models import CourierCountry, CourierRegion, WeightCost, Zone, ZoneConnection
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from import_export.admin import ImportExportModelAdmin
|
|
7
|
+
|
|
8
|
+
AdminBase = ImportExportModelAdmin
|
|
9
|
+
except ImportError:
|
|
10
|
+
AdminBase = admin.ModelAdmin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@admin.register(CourierCountry)
|
|
14
|
+
class CourierCountryAdmin(AdminBase):
|
|
15
|
+
list_display = ("name", "code", "is_domestic")
|
|
16
|
+
list_filter = ("is_domestic",)
|
|
17
|
+
search_fields = ("name", "code")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@admin.register(CourierRegion)
|
|
21
|
+
class CourierRegionAdmin(AdminBase):
|
|
22
|
+
list_display = ("name", "country", "code")
|
|
23
|
+
list_filter = ("country",)
|
|
24
|
+
search_fields = ("name", "code", "country__name")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@admin.register(Zone)
|
|
28
|
+
class ZoneAdmin(AdminBase):
|
|
29
|
+
list_display = ("name",)
|
|
30
|
+
search_fields = ("name",)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@admin.register(ZoneConnection)
|
|
34
|
+
class ZoneConnectionAdmin(AdminBase):
|
|
35
|
+
list_display = ("zone",)
|
|
36
|
+
filter_horizontal = ("countries", "regions")
|
|
37
|
+
search_fields = ("zone__name", "countries__name", "regions__name")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@admin.register(WeightCost)
|
|
41
|
+
class WeightCostAdmin(AdminBase):
|
|
42
|
+
list_display = ("zone", "weight", "amount")
|
|
43
|
+
list_filter = ("zone",)
|
|
44
|
+
search_fields = ("zone__name",)
|
courier/apps.py
ADDED
courier/calculator.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Generic courier pricing logic — usable from views, checkout, or Celery tasks."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
4
|
+
|
|
5
|
+
from django.db.models import Q
|
|
6
|
+
|
|
7
|
+
from .conf import get_country_model, get_region_model, get_setting
|
|
8
|
+
from .models import WeightCost, ZoneConnection
|
|
9
|
+
|
|
10
|
+
GRAMS_PER_LB = Decimal("453.592")
|
|
11
|
+
GRAMS_PER_KG = Decimal("1000")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CourierCalculationError(Exception):
|
|
15
|
+
"""Raised when shipping cost cannot be calculated."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_weight(weight, weight_unit):
|
|
19
|
+
unit = (weight_unit or get_setting("DEFAULT_WEIGHT_UNIT")).lower()
|
|
20
|
+
supported = get_setting("SUPPORTED_WEIGHT_UNITS")
|
|
21
|
+
if unit not in supported:
|
|
22
|
+
raise CourierCalculationError(
|
|
23
|
+
f"Unknown weight unit {weight_unit!r}. Supported: {', '.join(supported)}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
value = Decimal(str(weight))
|
|
27
|
+
if unit == "lbs":
|
|
28
|
+
return value * GRAMS_PER_LB
|
|
29
|
+
if unit == "kgs":
|
|
30
|
+
return value * GRAMS_PER_KG
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_domestic_country(country):
|
|
35
|
+
if hasattr(country, "is_domestic"):
|
|
36
|
+
return bool(country.is_domestic)
|
|
37
|
+
return country.name.lower() == get_setting("DOMESTIC_COUNTRY_NAME").lower()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_zone_connection(country_id, region_id=None):
|
|
41
|
+
custom = get_setting("ZONE_RESOLVER")
|
|
42
|
+
if custom:
|
|
43
|
+
return custom(country_id, region_id)
|
|
44
|
+
|
|
45
|
+
country_model = get_country_model()
|
|
46
|
+
country = country_model.objects.get(pk=country_id)
|
|
47
|
+
domestic = is_domestic_country(country)
|
|
48
|
+
|
|
49
|
+
if domestic and region_id:
|
|
50
|
+
region_model = get_region_model()
|
|
51
|
+
region = region_model.objects.get(pk=region_id)
|
|
52
|
+
connection = ZoneConnection.objects.filter(regions=region).first()
|
|
53
|
+
if connection:
|
|
54
|
+
return connection, False
|
|
55
|
+
|
|
56
|
+
connection = ZoneConnection.objects.filter(countries=country).first()
|
|
57
|
+
if not connection:
|
|
58
|
+
raise CourierCalculationError(
|
|
59
|
+
f"No shipping zone configured for country id={country_id}."
|
|
60
|
+
)
|
|
61
|
+
return connection, not domestic
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def billable_weight_kg(total_weight_grams):
|
|
65
|
+
threshold = Decimal(str(get_setting("HEAVY_WEIGHT_THRESHOLD_KG")))
|
|
66
|
+
padding = Decimal(str(get_setting("WEIGHT_PADDING_KG")))
|
|
67
|
+
total_kg = total_weight_grams / GRAMS_PER_KG
|
|
68
|
+
|
|
69
|
+
if total_kg > threshold:
|
|
70
|
+
return total_kg + padding
|
|
71
|
+
return total_kg + padding
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def lookup_weight_cost(zone, weight_kg):
|
|
75
|
+
increment = Decimal(str(get_setting("WEIGHT_INCREMENT_KG")))
|
|
76
|
+
qst = Q(weight__lt=weight_kg + increment) & Q(weight__gte=weight_kg)
|
|
77
|
+
return WeightCost.objects.filter(zone=zone).filter(qst).first()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def apply_pricing(weight_kg, cost, *, domestic_pricing, apply_surcharge):
|
|
81
|
+
amount = Decimal("0")
|
|
82
|
+
if not cost:
|
|
83
|
+
return amount
|
|
84
|
+
|
|
85
|
+
if domestic_pricing and get_setting("DOMESTIC_USE_WEIGHT_MULTIPLIER"):
|
|
86
|
+
increment = Decimal(str(get_setting("WEIGHT_INCREMENT_KG")))
|
|
87
|
+
multiplier = Decimal(str(weight_kg)) / increment
|
|
88
|
+
amount = cost.amount * multiplier
|
|
89
|
+
else:
|
|
90
|
+
amount = cost.amount
|
|
91
|
+
if apply_surcharge:
|
|
92
|
+
percent = Decimal(str(get_setting("INTERNATIONAL_SURCHARGE_PERCENT")))
|
|
93
|
+
amount = amount + (amount * percent / Decimal("100"))
|
|
94
|
+
return amount
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def calculate_courier_cost(
|
|
98
|
+
*,
|
|
99
|
+
weight,
|
|
100
|
+
quantity=1,
|
|
101
|
+
weight_unit=None,
|
|
102
|
+
country_id,
|
|
103
|
+
region_id=None,
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Generic shipping cost calculator.
|
|
107
|
+
|
|
108
|
+
Returns integer total (rounded) for the given destination and weight.
|
|
109
|
+
"""
|
|
110
|
+
grams = normalize_weight(weight, weight_unit or get_setting("DEFAULT_WEIGHT_UNIT"))
|
|
111
|
+
quantity = int(quantity)
|
|
112
|
+
if quantity < 1:
|
|
113
|
+
raise CourierCalculationError("Quantity must be at least 1.")
|
|
114
|
+
|
|
115
|
+
connection, apply_surcharge = resolve_zone_connection(country_id, region_id)
|
|
116
|
+
domestic_pricing = not apply_surcharge
|
|
117
|
+
weight_kg = billable_weight_kg(grams * quantity)
|
|
118
|
+
cost = lookup_weight_cost(connection.zone, weight_kg)
|
|
119
|
+
total = apply_pricing(
|
|
120
|
+
weight_kg,
|
|
121
|
+
cost,
|
|
122
|
+
domestic_pricing=domestic_pricing,
|
|
123
|
+
apply_surcharge=apply_surcharge,
|
|
124
|
+
)
|
|
125
|
+
return int(total.quantize(Decimal("1"), rounding=ROUND_HALF_UP))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def courier_cost(weight, quantity, weight_unit, country, state=None):
|
|
129
|
+
"""Backward-compatible alias used by existing integrations."""
|
|
130
|
+
return calculate_courier_cost(
|
|
131
|
+
weight=weight,
|
|
132
|
+
quantity=quantity,
|
|
133
|
+
weight_unit=weight_unit,
|
|
134
|
+
country_id=country,
|
|
135
|
+
region_id=state,
|
|
136
|
+
)
|
courier/conf.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Configurable settings for the courier Django app — Nitesh Kumar Singh (nkscoder)."""
|
|
2
|
+
|
|
3
|
+
from django.apps import apps
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULTS = {
|
|
9
|
+
"AUTHOR_NAME": "Nitesh Kumar Singh",
|
|
10
|
+
"AUTHOR_HANDLE": "nkscoder",
|
|
11
|
+
"GITHUB_URL": "https://github.com/nkscoder/courier",
|
|
12
|
+
"PYPI_URL": "https://pypi.org/project/nkscoder-courier/",
|
|
13
|
+
"SEO_SITE_NAME": "Django Courier Shipping Calculator",
|
|
14
|
+
"SEO_DESCRIPTION": (
|
|
15
|
+
"Generic open-source Django courier and shipping cost calculator with "
|
|
16
|
+
"zone-based weight pricing, domestic region zones, and international "
|
|
17
|
+
"surcharge support. Built by Nitesh Kumar Singh (nkscoder)."
|
|
18
|
+
),
|
|
19
|
+
"SEO_KEYWORDS": (
|
|
20
|
+
"django courier, shipping cost calculator, zone-based shipping, "
|
|
21
|
+
"weight-based courier pricing, generic django shipping, django shipping app, "
|
|
22
|
+
"nkscoder, nitesh kumar singh, python courier, ecommerce shipping"
|
|
23
|
+
),
|
|
24
|
+
# Location models (override to plug into your own Country/State tables)
|
|
25
|
+
"COUNTRY_MODEL": "courier.CourierCountry",
|
|
26
|
+
"REGION_MODEL": "courier.CourierRegion",
|
|
27
|
+
# Pricing defaults
|
|
28
|
+
"DOMESTIC_COUNTRY_NAME": "India",
|
|
29
|
+
"INTERNATIONAL_SURCHARGE_PERCENT": 22,
|
|
30
|
+
"HEAVY_WEIGHT_THRESHOLD_KG": 20,
|
|
31
|
+
"WEIGHT_PADDING_KG": "0.500",
|
|
32
|
+
"WEIGHT_INCREMENT_KG": "0.500",
|
|
33
|
+
"DEFAULT_WEIGHT_UNIT": "gram",
|
|
34
|
+
"SUPPORTED_WEIGHT_UNITS": ("gram", "kgs", "lbs"),
|
|
35
|
+
"DOMESTIC_USE_WEIGHT_MULTIPLIER": True,
|
|
36
|
+
# Optional hook: callable(country_id, region_id) -> (zone_connection, apply_surcharge)
|
|
37
|
+
"ZONE_RESOLVER": None,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_setting(name):
|
|
42
|
+
key = f"COURIER_{name}"
|
|
43
|
+
if hasattr(settings, key):
|
|
44
|
+
return getattr(settings, key)
|
|
45
|
+
return DEFAULTS[name]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_model(label):
|
|
49
|
+
try:
|
|
50
|
+
return apps.get_model(label)
|
|
51
|
+
except (LookupError, ValueError) as exc:
|
|
52
|
+
raise ImproperlyConfigured(f"COURIER model {label!r} is not installed.") from exc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_country_model():
|
|
56
|
+
return get_model(get_setting("COUNTRY_MODEL"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_region_model():
|
|
60
|
+
return get_model(get_setting("REGION_MODEL"))
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Generated by Django 3.0.8 on 2023-01-13 12:57
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
import django.utils.timezone
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="CourierCountry",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"id",
|
|
20
|
+
models.AutoField(
|
|
21
|
+
auto_created=True,
|
|
22
|
+
primary_key=True,
|
|
23
|
+
serialize=False,
|
|
24
|
+
verbose_name="ID",
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
|
28
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
29
|
+
("name", models.CharField(max_length=150, unique=True)),
|
|
30
|
+
("code", models.CharField(blank=True, max_length=10)),
|
|
31
|
+
(
|
|
32
|
+
"is_domestic",
|
|
33
|
+
models.BooleanField(
|
|
34
|
+
default=False,
|
|
35
|
+
help_text="Domestic destinations can use region-level zones and weight multipliers.",
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
options={
|
|
40
|
+
"verbose_name": "Courier Country",
|
|
41
|
+
"verbose_name_plural": "Courier Countries",
|
|
42
|
+
"ordering": ("name",),
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
migrations.CreateModel(
|
|
46
|
+
name="Zone",
|
|
47
|
+
fields=[
|
|
48
|
+
(
|
|
49
|
+
"id",
|
|
50
|
+
models.AutoField(
|
|
51
|
+
auto_created=True,
|
|
52
|
+
primary_key=True,
|
|
53
|
+
serialize=False,
|
|
54
|
+
verbose_name="ID",
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
|
58
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
59
|
+
("name", models.CharField(max_length=150)),
|
|
60
|
+
],
|
|
61
|
+
options={
|
|
62
|
+
"verbose_name": "Zone",
|
|
63
|
+
"verbose_name_plural": "Zones",
|
|
64
|
+
"ordering": ("name",),
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
migrations.CreateModel(
|
|
68
|
+
name="CourierRegion",
|
|
69
|
+
fields=[
|
|
70
|
+
(
|
|
71
|
+
"id",
|
|
72
|
+
models.AutoField(
|
|
73
|
+
auto_created=True,
|
|
74
|
+
primary_key=True,
|
|
75
|
+
serialize=False,
|
|
76
|
+
verbose_name="ID",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
|
80
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
81
|
+
("name", models.CharField(max_length=150)),
|
|
82
|
+
("code", models.CharField(blank=True, max_length=10)),
|
|
83
|
+
(
|
|
84
|
+
"country",
|
|
85
|
+
models.ForeignKey(
|
|
86
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
87
|
+
related_name="regions",
|
|
88
|
+
to="courier.couriercountry",
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
],
|
|
92
|
+
options={
|
|
93
|
+
"verbose_name": "Courier Region",
|
|
94
|
+
"verbose_name_plural": "Courier Regions",
|
|
95
|
+
"ordering": ("country__name", "name"),
|
|
96
|
+
"unique_together": {("country", "name")},
|
|
97
|
+
},
|
|
98
|
+
),
|
|
99
|
+
migrations.CreateModel(
|
|
100
|
+
name="ZoneConnection",
|
|
101
|
+
fields=[
|
|
102
|
+
(
|
|
103
|
+
"id",
|
|
104
|
+
models.AutoField(
|
|
105
|
+
auto_created=True,
|
|
106
|
+
primary_key=True,
|
|
107
|
+
serialize=False,
|
|
108
|
+
verbose_name="ID",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
|
112
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
113
|
+
(
|
|
114
|
+
"countries",
|
|
115
|
+
models.ManyToManyField(
|
|
116
|
+
blank=True,
|
|
117
|
+
related_name="zone_connections",
|
|
118
|
+
to="courier.couriercountry",
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
"regions",
|
|
123
|
+
models.ManyToManyField(
|
|
124
|
+
blank=True,
|
|
125
|
+
related_name="zone_connections",
|
|
126
|
+
to="courier.courierregion",
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
(
|
|
130
|
+
"zone",
|
|
131
|
+
models.ForeignKey(
|
|
132
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
133
|
+
related_name="connections",
|
|
134
|
+
to="courier.zone",
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
],
|
|
138
|
+
options={
|
|
139
|
+
"verbose_name": "Zone Connection",
|
|
140
|
+
"verbose_name_plural": "Zone Connections",
|
|
141
|
+
},
|
|
142
|
+
),
|
|
143
|
+
migrations.CreateModel(
|
|
144
|
+
name="WeightCost",
|
|
145
|
+
fields=[
|
|
146
|
+
(
|
|
147
|
+
"id",
|
|
148
|
+
models.AutoField(
|
|
149
|
+
auto_created=True,
|
|
150
|
+
primary_key=True,
|
|
151
|
+
serialize=False,
|
|
152
|
+
verbose_name="ID",
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
|
156
|
+
("updated_at", models.DateTimeField(auto_now=True)),
|
|
157
|
+
("weight", models.DecimalField(decimal_places=3, max_digits=20)),
|
|
158
|
+
("amount", models.DecimalField(decimal_places=6, max_digits=20)),
|
|
159
|
+
(
|
|
160
|
+
"zone",
|
|
161
|
+
models.ForeignKey(
|
|
162
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
163
|
+
related_name="weight_costs",
|
|
164
|
+
to="courier.zone",
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
],
|
|
168
|
+
options={
|
|
169
|
+
"verbose_name": "Weight Cost",
|
|
170
|
+
"verbose_name_plural": "Weight Costs",
|
|
171
|
+
"ordering": ("zone__name", "weight"),
|
|
172
|
+
},
|
|
173
|
+
),
|
|
174
|
+
]
|
|
File without changes
|
courier/models.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
|
|
4
|
+
class Base(models.Model):
|
|
5
|
+
created_at = models.DateTimeField(default=timezone.now)
|
|
6
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
7
|
+
|
|
8
|
+
class Meta:
|
|
9
|
+
abstract = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CourierCountry(Base):
|
|
13
|
+
"""Generic country/destination used for zone mapping."""
|
|
14
|
+
|
|
15
|
+
name = models.CharField(max_length=150, unique=True)
|
|
16
|
+
code = models.CharField(max_length=10, blank=True)
|
|
17
|
+
is_domestic = models.BooleanField(
|
|
18
|
+
default=False,
|
|
19
|
+
help_text="Domestic destinations can use region-level zones and weight multipliers.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
class Meta:
|
|
23
|
+
verbose_name = "Courier Country"
|
|
24
|
+
verbose_name_plural = "Courier Countries"
|
|
25
|
+
ordering = ("name",)
|
|
26
|
+
|
|
27
|
+
def __str__(self):
|
|
28
|
+
return self.name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CourierRegion(Base):
|
|
32
|
+
"""Generic state/province/region under a country."""
|
|
33
|
+
|
|
34
|
+
name = models.CharField(max_length=150)
|
|
35
|
+
code = models.CharField(max_length=10, blank=True)
|
|
36
|
+
country = models.ForeignKey(
|
|
37
|
+
CourierCountry,
|
|
38
|
+
on_delete=models.CASCADE,
|
|
39
|
+
related_name="regions",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
class Meta:
|
|
43
|
+
verbose_name = "Courier Region"
|
|
44
|
+
verbose_name_plural = "Courier Regions"
|
|
45
|
+
ordering = ("country__name", "name")
|
|
46
|
+
unique_together = ("country", "name")
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return f"{self.name}, {self.country.name}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Zone(Base):
|
|
53
|
+
name = models.CharField(max_length=150)
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
verbose_name = "Zone"
|
|
57
|
+
verbose_name_plural = "Zones"
|
|
58
|
+
ordering = ("name",)
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return self.name
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ZoneConnection(Base):
|
|
65
|
+
zone = models.ForeignKey(Zone, on_delete=models.CASCADE, related_name="connections")
|
|
66
|
+
countries = models.ManyToManyField(
|
|
67
|
+
"courier.CourierCountry",
|
|
68
|
+
blank=True,
|
|
69
|
+
related_name="zone_connections",
|
|
70
|
+
)
|
|
71
|
+
regions = models.ManyToManyField(
|
|
72
|
+
"courier.CourierRegion",
|
|
73
|
+
blank=True,
|
|
74
|
+
related_name="zone_connections",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
class Meta:
|
|
78
|
+
verbose_name = "Zone Connection"
|
|
79
|
+
verbose_name_plural = "Zone Connections"
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return str(self.zone)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class WeightCost(Base):
|
|
86
|
+
zone = models.ForeignKey(Zone, on_delete=models.CASCADE, related_name="weight_costs")
|
|
87
|
+
weight = models.DecimalField(max_digits=20, decimal_places=3)
|
|
88
|
+
amount = models.DecimalField(max_digits=20, decimal_places=6)
|
|
89
|
+
|
|
90
|
+
class Meta:
|
|
91
|
+
verbose_name = "Weight Cost"
|
|
92
|
+
verbose_name_plural = "Weight Costs"
|
|
93
|
+
ordering = ("zone__name", "weight")
|
|
94
|
+
|
|
95
|
+
def __str__(self):
|
|
96
|
+
return f"{self.zone} @ {self.weight} = {self.amount}"
|
courier/test_settings.py
ADDED
courier/tests.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from django.test import SimpleTestCase, override_settings
|
|
5
|
+
|
|
6
|
+
from courier.calculator import apply_pricing, normalize_weight
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NormalizeWeightTests(SimpleTestCase):
|
|
10
|
+
def test_grams(self):
|
|
11
|
+
self.assertEqual(normalize_weight(500, "gram"), Decimal("500"))
|
|
12
|
+
|
|
13
|
+
def test_kgs(self):
|
|
14
|
+
self.assertEqual(normalize_weight(1.5, "kgs"), Decimal("1500"))
|
|
15
|
+
|
|
16
|
+
def test_lbs(self):
|
|
17
|
+
self.assertEqual(normalize_weight(1, "lbs"), Decimal("453.592"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@override_settings(
|
|
21
|
+
COURIER_DOMESTIC_USE_WEIGHT_MULTIPLIER=True,
|
|
22
|
+
COURIER_WEIGHT_INCREMENT_KG="0.500",
|
|
23
|
+
COURIER_INTERNATIONAL_SURCHARGE_PERCENT=22,
|
|
24
|
+
)
|
|
25
|
+
class ApplyPricingTests(SimpleTestCase):
|
|
26
|
+
def test_domestic_multiplier(self):
|
|
27
|
+
cost = MagicMock()
|
|
28
|
+
cost.amount = Decimal("10")
|
|
29
|
+
amount = apply_pricing(
|
|
30
|
+
Decimal("2.0"),
|
|
31
|
+
cost,
|
|
32
|
+
domestic_pricing=True,
|
|
33
|
+
apply_surcharge=False,
|
|
34
|
+
)
|
|
35
|
+
self.assertEqual(amount, Decimal("40"))
|
|
36
|
+
|
|
37
|
+
def test_international_surcharge(self):
|
|
38
|
+
cost = MagicMock()
|
|
39
|
+
cost.amount = Decimal("100")
|
|
40
|
+
amount = apply_pricing(
|
|
41
|
+
Decimal("1.0"),
|
|
42
|
+
cost,
|
|
43
|
+
domestic_pricing=False,
|
|
44
|
+
apply_surcharge=True,
|
|
45
|
+
)
|
|
46
|
+
self.assertEqual(amount, Decimal("122"))
|
courier/urls.py
ADDED
courier/views.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django.http import HttpResponse, JsonResponse
|
|
4
|
+
|
|
5
|
+
from .calculator import CourierCalculationError, calculate_courier_cost
|
|
6
|
+
from .conf import get_setting
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def courier(request):
|
|
10
|
+
"""JSON endpoint: calculate shipping cost from query params."""
|
|
11
|
+
try:
|
|
12
|
+
total = calculate_courier_cost(
|
|
13
|
+
weight=request.GET.get("weight", 0),
|
|
14
|
+
quantity=request.GET.get("quantity", 1),
|
|
15
|
+
weight_unit=request.GET.get("weight_unit") or get_setting("DEFAULT_WEIGHT_UNIT"),
|
|
16
|
+
country_id=request.GET.get("country"),
|
|
17
|
+
region_id=request.GET.get("state") or request.GET.get("region"),
|
|
18
|
+
)
|
|
19
|
+
except CourierCalculationError as exc:
|
|
20
|
+
return JsonResponse({"error": str(exc)}, status=400)
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
return JsonResponse({"error": str(exc)}, status=400)
|
|
23
|
+
|
|
24
|
+
return HttpResponse(
|
|
25
|
+
json.dumps({"total": total}),
|
|
26
|
+
content_type="application/json",
|
|
27
|
+
status=200,
|
|
28
|
+
)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nkscoder-courier
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Django zone-based courier shipping cost calculator — by Nitesh Kumar Singh (nkscoder)
|
|
5
|
+
Author-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
6
|
+
Maintainer-email: Nitesh Kumar Singh <nkscoder@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/nkscoder/courier
|
|
9
|
+
Project-URL: Documentation, https://github.com/nkscoder/courier#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/nkscoder/courier
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/nkscoder/courier/issues
|
|
12
|
+
Project-URL: Author GitHub, https://github.com/nkscoder
|
|
13
|
+
Project-URL: PyPI, https://pypi.org/project/nkscoder-courier/
|
|
14
|
+
Keywords: django,courier,shipping,shipping-calculator,zone-based-shipping,weight-based-pricing,india-courier,ecommerce,logistics,nkscoder,nitesh-kumar-singh,python,django-courier,shipping-cost,delivery-pricing
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Environment :: Web Environment
|
|
17
|
+
Classifier: Framework :: Django
|
|
18
|
+
Classifier: Framework :: Django :: 3.2
|
|
19
|
+
Classifier: Framework :: Django :: 4.0
|
|
20
|
+
Classifier: Framework :: Django :: 4.1
|
|
21
|
+
Classifier: Framework :: Django :: 4.2
|
|
22
|
+
Classifier: Framework :: Django :: 5.0
|
|
23
|
+
Classifier: Framework :: Django :: 5.1
|
|
24
|
+
Classifier: Framework :: Django :: 5.2
|
|
25
|
+
Classifier: Intended Audience :: Developers
|
|
26
|
+
Classifier: Operating System :: OS Independent
|
|
27
|
+
Classifier: Programming Language :: Python
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
34
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
35
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
36
|
+
Requires-Python: >=3.8
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Requires-Dist: Django>=3.2
|
|
40
|
+
Provides-Extra: admin
|
|
41
|
+
Requires-Dist: django-import-export>=3.0; extra == "admin"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Courier — Generic Django Zone-Based Shipping Cost Calculator
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/nkscoder-courier/)
|
|
47
|
+
[](https://pypi.org/project/nkscoder-courier/)
|
|
48
|
+
[](https://www.python.org/downloads/)
|
|
49
|
+
[](https://www.djangoproject.com/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
**Author:** [Nitesh Kumar Singh](https://github.com/nkscoder) · **GitHub:** [@nkscoder](https://github.com/nkscoder)
|
|
53
|
+
|
|
54
|
+
A **generic, reusable Django courier app** for calculating zone-based shipping costs by weight, quantity, and destination. Works out of the box with built-in country/region models, or plugs into your existing location tables. Built by **Nitesh Kumar Singh (nkscoder)**.
|
|
55
|
+
|
|
56
|
+
> **Keywords:** django courier · generic shipping calculator · zone-based shipping · weight pricing · reusable django app · nkscoder · nitesh kumar singh · python shipping
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Generic by design** — built-in `CourierCountry` / `CourierRegion` models, no `core` app required
|
|
63
|
+
- **Configurable pricing** — domestic multiplier, international surcharge %, weight units, thresholds
|
|
64
|
+
- **Zone-based pricing** — map countries and regions to shipping zones
|
|
65
|
+
- **Weight slab lookup** — automatic cost lookup by weight brackets
|
|
66
|
+
- **Multi-unit support** — grams, kilograms, and pounds (`gram`, `kgs`, `lbs`)
|
|
67
|
+
- **JSON API endpoint** — real-time shipping quotes via HTTP GET
|
|
68
|
+
- **Python API** — call `calculate_courier_cost()` from checkout, cart, or background jobs
|
|
69
|
+
- **Custom zone resolver** — plug in your own location models with one settings hook
|
|
70
|
+
- **Django Admin** — manage countries, regions, zones, and weight costs
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Python 3.8–3.12 (including **3.12**)
|
|
77
|
+
- Django 3.2+
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install nkscoder-courier
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With Django Admin CSV import/export:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install "nkscoder-courier[admin]"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
From GitHub:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install git+https://github.com/nkscoder/courier.git
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# settings.py
|
|
105
|
+
INSTALLED_APPS = [
|
|
106
|
+
...
|
|
107
|
+
"courier",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
COURIER_DOMESTIC_COUNTRY_NAME = "India"
|
|
111
|
+
COURIER_INTERNATIONAL_SURCHARGE_PERCENT = 22
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python manage.py migrate courier
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
# urls.py
|
|
120
|
+
urlpatterns = [
|
|
121
|
+
path("courier/", include("courier.urls")),
|
|
122
|
+
]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
### Python API (recommended)
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from courier.calculator import calculate_courier_cost
|
|
133
|
+
|
|
134
|
+
total = calculate_courier_cost(
|
|
135
|
+
weight=1.5,
|
|
136
|
+
quantity=2,
|
|
137
|
+
weight_unit="kgs",
|
|
138
|
+
country_id=country.pk,
|
|
139
|
+
region_id=region.pk, # optional for domestic regions
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Backward-compatible alias:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from courier import courier_cost
|
|
147
|
+
|
|
148
|
+
total = courier_cost(weight=500, quantity=1, weight_unit="gram", country=1, state=3)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### HTTP API
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
GET /courier/?weight=500&quantity=2&weight_unit=gram&country=1®ion=3
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Response:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{"total": 150}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Parameter | Description |
|
|
164
|
+
|-----------|-------------|
|
|
165
|
+
| `weight` | Package weight |
|
|
166
|
+
| `quantity` | Number of packages |
|
|
167
|
+
| `weight_unit` | `gram`, `kgs`, or `lbs` |
|
|
168
|
+
| `country` | Country PK |
|
|
169
|
+
| `region` or `state` | Region PK (for domestic destinations) |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Configuration
|
|
174
|
+
|
|
175
|
+
All settings use the `COURIER_` prefix:
|
|
176
|
+
|
|
177
|
+
| Setting | Default | Description |
|
|
178
|
+
|---------|---------|-------------|
|
|
179
|
+
| `COURIER_COUNTRY_MODEL` | `courier.CourierCountry` | Country model label |
|
|
180
|
+
| `COURIER_REGION_MODEL` | `courier.CourierRegion` | Region/state model label |
|
|
181
|
+
| `COURIER_DOMESTIC_COUNTRY_NAME` | `India` | Fallback domestic country name |
|
|
182
|
+
| `COURIER_INTERNATIONAL_SURCHARGE_PERCENT` | `22` | Surcharge for non-domestic destinations |
|
|
183
|
+
| `COURIER_HEAVY_WEIGHT_THRESHOLD_KG` | `20` | Heavy shipment threshold |
|
|
184
|
+
| `COURIER_WEIGHT_PADDING_KG` | `0.500` | Padding added to billable weight |
|
|
185
|
+
| `COURIER_WEIGHT_INCREMENT_KG` | `0.500` | Domestic weight multiplier increment |
|
|
186
|
+
| `COURIER_DEFAULT_WEIGHT_UNIT` | `gram` | Default unit for API requests |
|
|
187
|
+
| `COURIER_DOMESTIC_USE_WEIGHT_MULTIPLIER` | `True` | Per-increment pricing for domestic |
|
|
188
|
+
| `COURIER_ZONE_RESOLVER` | `None` | Custom `(country_id, region_id) -> (connection, surcharge)` |
|
|
189
|
+
|
|
190
|
+
### Plug into your own location models
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# settings.py
|
|
194
|
+
COURIER_COUNTRY_MODEL = "myapp.Country"
|
|
195
|
+
COURIER_REGION_MODEL = "myapp.State"
|
|
196
|
+
|
|
197
|
+
def resolve_courier_zone(country_id, region_id):
|
|
198
|
+
from courier.models import ZoneConnection
|
|
199
|
+
# your lookup logic
|
|
200
|
+
return connection, apply_surcharge
|
|
201
|
+
|
|
202
|
+
COURIER_ZONE_RESOLVER = resolve_courier_zone
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Mark domestic countries with `is_domestic=True` on your model, or rely on `COURIER_DOMESTIC_COUNTRY_NAME`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Admin setup
|
|
210
|
+
|
|
211
|
+
1. **Courier Country** — add destinations; mark domestic with `is_domestic`
|
|
212
|
+
2. **Courier Region** — add states/provinces under domestic countries
|
|
213
|
+
3. **Zone** — create shipping zones (Zone A, Zone B, …)
|
|
214
|
+
4. **Zone Connection** — link zones to countries and/or regions
|
|
215
|
+
5. **Weight Cost** — set price per weight slab for each zone
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## How pricing works
|
|
220
|
+
|
|
221
|
+
1. Resolve the destination **zone** from country/region via `ZoneConnection`
|
|
222
|
+
2. Normalize weight to grams and compute total billable weight
|
|
223
|
+
3. Look up the matching **WeightCost** slab for that zone
|
|
224
|
+
4. **Domestic**: multiply slab rate by weight increments (configurable)
|
|
225
|
+
5. **International**: apply configurable **surcharge %** on the base rate
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
git clone git@github.com:nkscoder/courier.git
|
|
233
|
+
cd courier
|
|
234
|
+
pip install -e ".[admin]"
|
|
235
|
+
python manage.py migrate
|
|
236
|
+
python manage.py test courier
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Changelog
|
|
242
|
+
|
|
243
|
+
### 1.1.1
|
|
244
|
+
- Renamed PyPI package to `nkscoder-courier` (name `courier` was taken on PyPI)
|
|
245
|
+
- Fixed GitHub Actions publish workflow with `PYPI_API_TOKEN`
|
|
246
|
+
|
|
247
|
+
### 1.1.0
|
|
248
|
+
- Generic built-in `CourierCountry` / `CourierRegion` models (no `core` dependency)
|
|
249
|
+
- New `calculate_courier_cost()` service API
|
|
250
|
+
- Configurable settings via `COURIER_*` prefix
|
|
251
|
+
- Optional custom `COURIER_ZONE_RESOLVER` hook
|
|
252
|
+
- Improved admin with search and filters
|
|
253
|
+
|
|
254
|
+
### 1.0.0
|
|
255
|
+
- Initial PyPI release by **Nitesh Kumar Singh (nkscoder)**
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Author & Links
|
|
260
|
+
|
|
261
|
+
| | |
|
|
262
|
+
|---|---|
|
|
263
|
+
| **Author** | Nitesh Kumar Singh |
|
|
264
|
+
| **GitHub** | [github.com/nkscoder](https://github.com/nkscoder) |
|
|
265
|
+
| **Repository** | [github.com/nkscoder/courier](https://github.com/nkscoder/courier) |
|
|
266
|
+
| **PyPI** | [pypi.org/project/nkscoder-courier](https://pypi.org/project/nkscoder-courier) |
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License — Copyright (c) 2020-2026 [Nitesh Kumar Singh (nkscoder)](https://github.com/nkscoder)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
courier/__init__.py,sha256=c4hYpuflDF0dhErloZVwCTmSWHSFQ_DojFHc0Tb-B0M,630
|
|
2
|
+
courier/admin.py,sha256=E0GKoEL5cWOywvt9104tHmdkklnwjcJ44bwozToOhOA,1181
|
|
3
|
+
courier/apps.py,sha256=QfaVOAt60KAzjtwlnwjBORl92oF5LI4rQ2BfyNUrViw,184
|
|
4
|
+
courier/calculator.py,sha256=P07i0NfRf5D49jAfrwFJyaWiUPfOCidkxCtWDVCzgmU,4388
|
|
5
|
+
courier/conf.py,sha256=VE7J_82YLNb8NlLDs0Lu3bwLbVbJbUakdFijABe7mww,2140
|
|
6
|
+
courier/models.py,sha256=b32nFWoUfmJb0GTbVXutH3y2L5ThHmZSel4g5JvvjmY,2645
|
|
7
|
+
courier/test_settings.py,sha256=pmuEqUJb9RHcAoeXCmgGM5M6CWZkfgNq10FzOmSXuEs,231
|
|
8
|
+
courier/tests.py,sha256=4nr2ihsigxGW8SV5At89lzMBHAs0VkBJeue-pX9gyUo,1361
|
|
9
|
+
courier/urls.py,sha256=1wcC6_KI_dWdEfBY7RYr-vB_96ftWLWhLo20b72n01k,120
|
|
10
|
+
courier/views.py,sha256=GIM2p_uLOzDi_VePFH4i9dFUt6tLEx2Vwz2AyVu-GAw,967
|
|
11
|
+
courier/migrations/0001_initial.py,sha256=SsAuCoc9cwNbdgDuuRjeOcWe7k3LZm_TeoskqgvzKJ0,6251
|
|
12
|
+
courier/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
nkscoder_courier-1.1.1.dist-info/licenses/LICENSE,sha256=q8sNgEeghCI1fwxqOLrlqOnNZeVppp_xcwNdwlKu-Qk,1091
|
|
14
|
+
nkscoder_courier-1.1.1.dist-info/METADATA,sha256=6NFF93i-MAOpcNCJ1ZwEAkPM7NolNJ--6x1VGNNb6WA,8750
|
|
15
|
+
nkscoder_courier-1.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
16
|
+
nkscoder_courier-1.1.1.dist-info/top_level.txt,sha256=JCa2wS13MdVQXTpD3ijfo9rPfbHY7VWTYNBueh2imHU,8
|
|
17
|
+
nkscoder_courier-1.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Nitesh Kumar Singh (nkscoder)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
courier
|