Features: 1) Add SEOMetaType to support structured SEO metadata for brands, products, and categories; 2) Introduce resolve_seo_meta for dynamic SEO data generation on key models; 3) Add helper functions graphene_current_lang and graphene_abs for language-based URLs and absolute URI handling.
Fixes: None; Extra: 1) Refactor utility docstrings for clarity and detail; 2) Modernize utility function logic and improve inline comments; 3) Adjust logging initialization in `object_types.py`.
This commit is contained in:
parent
ab10a7a0b7
commit
91ebaece07
2 changed files with 310 additions and 42 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from constance import config
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import Max, Min, QuerySet
|
from django.db.models import Max, Min, QuerySet
|
||||||
from django.db.models.functions import Length
|
from django.db.models.functions import Length
|
||||||
|
|
@ -41,10 +44,31 @@ from core.models import (
|
||||||
Vendor,
|
Vendor,
|
||||||
Wishlist,
|
Wishlist,
|
||||||
)
|
)
|
||||||
|
from core.utils import graphene_current_lang, graphene_abs
|
||||||
|
from core.utils.seo_builders import (
|
||||||
|
org_schema,
|
||||||
|
breadcrumb_schema,
|
||||||
|
brand_schema,
|
||||||
|
website_schema,
|
||||||
|
category_schema,
|
||||||
|
item_list_schema,
|
||||||
|
product_schema,
|
||||||
|
)
|
||||||
from payments.graphene.object_types import TransactionType
|
from payments.graphene.object_types import TransactionType
|
||||||
from payments.models import Transaction
|
from payments.models import Transaction
|
||||||
|
|
||||||
logger = __import__("logging").getLogger(__name__)
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
|
||||||
|
class SEOMetaType(ObjectType):
|
||||||
|
title = String()
|
||||||
|
description = String()
|
||||||
|
canonical = String()
|
||||||
|
robots = String()
|
||||||
|
open_graph = GenericScalar()
|
||||||
|
twitter = GenericScalar()
|
||||||
|
json_ld = List(GenericScalar)
|
||||||
|
hreflang = List(GenericScalar)
|
||||||
|
|
||||||
|
|
||||||
class AttributeType(DjangoObjectType):
|
class AttributeType(DjangoObjectType):
|
||||||
|
|
@ -90,6 +114,7 @@ class AttributeGroupType(DjangoObjectType):
|
||||||
|
|
||||||
class BrandType(DjangoObjectType):
|
class BrandType(DjangoObjectType):
|
||||||
categories = List(lambda: CategoryType, description=_("categories"))
|
categories = List(lambda: CategoryType, description=_("categories"))
|
||||||
|
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Brand
|
model = Brand
|
||||||
|
|
@ -109,6 +134,48 @@ class BrandType(DjangoObjectType):
|
||||||
def resolve_small_logo(self: Brand, info):
|
def resolve_small_logo(self: Brand, info):
|
||||||
return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""
|
return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""
|
||||||
|
|
||||||
|
def resolve_seo_meta(self: Brand, info):
|
||||||
|
lang = graphene_current_lang()
|
||||||
|
base = f"https://{config.BASE_DOMAIN}"
|
||||||
|
canonical = f"{base}/{lang}/brand/{self.slug}"
|
||||||
|
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||||
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
|
logo_url = None
|
||||||
|
if getattr(self, "big_logo", None):
|
||||||
|
logo_url = graphene_abs(info.context, self.big_logo.url)
|
||||||
|
elif getattr(self, "small_logo", None):
|
||||||
|
logo_url = graphene_abs(info.context, self.small_logo.url)
|
||||||
|
|
||||||
|
og = {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"type": "website",
|
||||||
|
"url": canonical,
|
||||||
|
"image": logo_url or "",
|
||||||
|
}
|
||||||
|
tw = {"card": "summary_large_image", "title": title, "description": description, "image": logo_url or ""}
|
||||||
|
|
||||||
|
crumbs = [("Home", f"{base}/"), (self.name, canonical)]
|
||||||
|
|
||||||
|
json_ld = [
|
||||||
|
org_schema(),
|
||||||
|
website_schema(),
|
||||||
|
breadcrumb_schema(crumbs),
|
||||||
|
brand_schema(self, canonical, logo_url=logo_url),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"canonical": canonical,
|
||||||
|
"robots": "index,follow",
|
||||||
|
"open_graph": og,
|
||||||
|
"twitter": tw,
|
||||||
|
"json_ld": json_ld,
|
||||||
|
"hreflang": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FilterableAttributeType(ObjectType):
|
class FilterableAttributeType(ObjectType):
|
||||||
attribute_name = String(required=True)
|
attribute_name = String(required=True)
|
||||||
|
|
@ -137,6 +204,7 @@ class CategoryType(DjangoObjectType):
|
||||||
)
|
)
|
||||||
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
|
tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
|
||||||
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
|
products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
|
||||||
|
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
model = Category
|
||||||
|
|
@ -219,6 +287,58 @@ class CategoryType(DjangoObjectType):
|
||||||
"max_price": min_max_prices["max_price"],
|
"max_price": min_max_prices["max_price"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def resolve_seo_meta(self: Category, info):
|
||||||
|
lang = graphene_current_lang()
|
||||||
|
base = f"https://{config.BASE_DOMAIN}"
|
||||||
|
canonical = f"{base}/{lang}/catalog/{self.slug}"
|
||||||
|
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||||
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
|
og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""
|
||||||
|
|
||||||
|
og = {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"type": "website",
|
||||||
|
"url": canonical,
|
||||||
|
"image": og_image,
|
||||||
|
}
|
||||||
|
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
|
||||||
|
|
||||||
|
crumbs = [("Home", f"{base}/")]
|
||||||
|
for c in self.get_ancestors():
|
||||||
|
crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
|
||||||
|
crumbs.append((self.name, canonical))
|
||||||
|
|
||||||
|
json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(self, canonical)]
|
||||||
|
|
||||||
|
product_urls = []
|
||||||
|
qs = (
|
||||||
|
Product.objects.filter(
|
||||||
|
is_active=True,
|
||||||
|
category=self,
|
||||||
|
brand__is_active=True,
|
||||||
|
stocks__vendor__is_active=True,
|
||||||
|
)
|
||||||
|
.only("slug")
|
||||||
|
.distinct()[:24]
|
||||||
|
)
|
||||||
|
for p in qs:
|
||||||
|
product_urls.append(f"{base}/{lang}/product/{p.slug}")
|
||||||
|
if product_urls:
|
||||||
|
json_ld.append(item_list_schema(product_urls))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"canonical": canonical,
|
||||||
|
"robots": "index,follow",
|
||||||
|
"open_graph": og,
|
||||||
|
"twitter": tw,
|
||||||
|
"json_ld": json_ld,
|
||||||
|
"hreflang": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VendorType(DjangoObjectType):
|
class VendorType(DjangoObjectType):
|
||||||
markup_percent = Float(description=_("markup percentage"))
|
markup_percent = Float(description=_("markup percentage"))
|
||||||
|
|
@ -380,6 +500,7 @@ class ProductType(DjangoObjectType):
|
||||||
quantity = Float(description=_("quantity"))
|
quantity = Float(description=_("quantity"))
|
||||||
feedbacks_count = Int(description=_("number of feedbacks"))
|
feedbacks_count = Int(description=_("number of feedbacks"))
|
||||||
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
||||||
|
seo_meta = Field(SEOMetaType, description=_("SEO meta snapshot"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
|
|
@ -425,6 +546,52 @@ class ProductType(DjangoObjectType):
|
||||||
def resolve_personal_orders_only(self: Product, _info) -> bool:
|
def resolve_personal_orders_only(self: Product, _info) -> bool:
|
||||||
return False or self.personal_orders_only
|
return False or self.personal_orders_only
|
||||||
|
|
||||||
|
def resolve_seo_meta(self: Product, info):
|
||||||
|
lang = graphene_current_lang()
|
||||||
|
base = f"https://{config.BASE_DOMAIN}"
|
||||||
|
canonical = f"{base}/{lang}/product/{self.slug}"
|
||||||
|
title = f"{self.name} | {config.PROJECT_NAME}"
|
||||||
|
description = (self.description or "")[:180]
|
||||||
|
|
||||||
|
first_img = self.images.order_by("priority").first()
|
||||||
|
og_image = _abs(info.context, first_img.image.url) if first_img else ""
|
||||||
|
|
||||||
|
og = {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"type": "product",
|
||||||
|
"url": canonical,
|
||||||
|
"image": og_image,
|
||||||
|
}
|
||||||
|
tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}
|
||||||
|
|
||||||
|
crumbs = [("Home", f"{base}/")]
|
||||||
|
if self.category:
|
||||||
|
for c in self.category.get_ancestors(include_self=True):
|
||||||
|
crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
|
||||||
|
crumbs.append((self.name, canonical))
|
||||||
|
|
||||||
|
images = list(self.images.all()[:6])
|
||||||
|
rating = {"value": self.rating, "count": self.feedbacks_count}
|
||||||
|
|
||||||
|
json_ld = [
|
||||||
|
org_schema(),
|
||||||
|
website_schema(),
|
||||||
|
breadcrumb_schema(crumbs),
|
||||||
|
product_schema(self, images, rating=rating),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"canonical": canonical,
|
||||||
|
"robots": "index,follow",
|
||||||
|
"open_graph": og,
|
||||||
|
"twitter": tw,
|
||||||
|
"json_ld": json_ld,
|
||||||
|
"hreflang": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AttributeValueType(DjangoObjectType):
|
class AttributeValueType(DjangoObjectType):
|
||||||
value = String(description=_("attribute value"))
|
value = String(description=_("attribute value"))
|
||||||
|
|
|
||||||
|
|
@ -4,68 +4,116 @@ import secrets
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from constance import config
|
from constance import config
|
||||||
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
from evibes.settings import DEBUG, EXPOSABLE_KEYS, LANGUAGE_CODE
|
from evibes.settings import DEBUG, EXPOSABLE_KEYS, LANGUAGE_CODE
|
||||||
|
|
||||||
logger = logging.getLogger("django")
|
logger = logging.getLogger("django")
|
||||||
|
|
||||||
|
|
||||||
|
def graphene_current_lang():
|
||||||
|
"""
|
||||||
|
Determines the currently active language code.
|
||||||
|
|
||||||
|
This function retrieves the current language from the available language
|
||||||
|
settings. If no language is set, it defaults to the application's default
|
||||||
|
language code. The language code is returned in lowercase.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The currently active language code in lowercase.
|
||||||
|
"""
|
||||||
|
return (get_language() or settings.LANGUAGE_CODE).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def graphene_abs(request, path_or_url: str) -> str:
|
||||||
|
"""
|
||||||
|
Builds and returns an absolute URI for a given path or URL.
|
||||||
|
|
||||||
|
Summary:
|
||||||
|
This function takes a relative path or URL and constructs a fully
|
||||||
|
qualified absolute URI using the request object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The request object used to build the absolute URI.
|
||||||
|
path_or_url: str
|
||||||
|
The relative path or URL to be converted to an absolute URI.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The absolute URI corresponding to the provided path or URL.
|
||||||
|
"""
|
||||||
|
return request.build_absolute_uri(path_or_url)
|
||||||
|
|
||||||
|
|
||||||
def get_random_code() -> str:
|
def get_random_code() -> str:
|
||||||
"""
|
"""
|
||||||
Generates a random string of a specified length. This method calls the
|
Generates a random alphanumeric string of a fixed length.
|
||||||
get_random_string function to create a random alphanumeric string of
|
|
||||||
20 characters in length.
|
|
||||||
|
|
||||||
Returns
|
This function uses a utility function to generate a random string
|
||||||
-------
|
consisting of letters and digits. The length of the string is fixed
|
||||||
str
|
to 20 characters. The generated string can be used for purposes
|
||||||
A 20-character-long alphanumeric string.
|
such as unique identifiers or tokens.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Randomly generated alphanumeric string of length 20.
|
||||||
"""
|
"""
|
||||||
return get_random_string(20)
|
return get_random_string(20)
|
||||||
|
|
||||||
|
|
||||||
def get_product_uuid_as_path(instance, filename: str = "") -> str:
|
def get_product_uuid_as_path(instance, filename: str = "") -> str:
|
||||||
"""
|
"""
|
||||||
Generates a unique file path for a product using its UUID.
|
Generates a file path for a product using its UUID.
|
||||||
|
|
||||||
This function constructs a file path that includes the product UUID
|
This function constructs a standardized file path where an uploaded file
|
||||||
in its directory structure. The path format is "products/{product_uuid}/{filename}",
|
is saved for a product. The path includes a `products` directory, followed
|
||||||
where `product_uuid` is derived from the instance's product attribute, and
|
by the product's UUID, and concludes with the original filename. It can be
|
||||||
`filename` corresponds to the original name of the file being processed.
|
utilized in file storage applications to ensure unique and organized file
|
||||||
|
storage based on the product's identity.
|
||||||
|
|
||||||
Parameters:
|
Args:
|
||||||
instance: Object
|
instance: The object instance that contains a reference to the product.
|
||||||
The model instance containing the product attribute with the desired UUID.
|
filename: str, optional. The name of the file being uploaded. Default is an
|
||||||
filename: str
|
empty string.
|
||||||
The original name of the file for which the path is being generated.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str
|
str: A string that represents the constructed file path.
|
||||||
A string representing the generated unique file path that adheres to the
|
|
||||||
format "products/{product_uuid}/{filename}".
|
|
||||||
"""
|
"""
|
||||||
return "products" + "/" + str(instance.product.uuid) + "/" + filename
|
return "products" + "/" + str(instance.product.uuid) + "/" + filename
|
||||||
|
|
||||||
|
|
||||||
def get_brand_name_as_path(instance, filename: str = "") -> str:
|
def get_brand_name_as_path(instance, filename: str = "") -> str:
|
||||||
|
"""
|
||||||
|
Generates a file path for a brand based on its name and the provided filename.
|
||||||
|
|
||||||
|
This function constructs a unique file path within the 'brands/' directory using
|
||||||
|
the name of the given instance and appends the supplied filename.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
instance: An object containing a 'name' attribute.
|
||||||
|
filename: str, optional. The name of the file to be appended to the path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A string representing the constructed file path.
|
||||||
|
"""
|
||||||
return "brands/" + str(instance.name) + "/" + filename
|
return "brands/" + str(instance.name) + "/" + filename
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def atomic_if_not_debug():
|
def atomic_if_not_debug():
|
||||||
"""
|
"""
|
||||||
A context manager to execute a database operation within an atomic transaction
|
Context manager to wrap a block of code in an atomic transaction if the DEBUG
|
||||||
when the `DEBUG` setting is disabled. If `DEBUG` is enabled, it bypasses
|
setting is not enabled.
|
||||||
transactional behavior. This allows safe rollback in production and easier
|
|
||||||
debugging in development.
|
|
||||||
|
|
||||||
Yields
|
This context manager ensures that the code block executes within an atomic
|
||||||
------
|
database transaction, preventing partial updates to the database in case of
|
||||||
None
|
an exception. If the DEBUG setting is enabled, no transaction is enforced,
|
||||||
Yields control to the enclosed block of code.
|
allowing for easier debugging.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
None: This context manager does not return any values.
|
||||||
"""
|
"""
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
|
|
@ -76,21 +124,37 @@ def atomic_if_not_debug():
|
||||||
|
|
||||||
def is_url_safe(url: str) -> bool:
|
def is_url_safe(url: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Determine if a given URL is safe. This function evaluates whether
|
Determines if a given URL starts with "https://" indicating it is a secure URL.
|
||||||
the provided URL starts with "https://", making it a potentially
|
|
||||||
secure resource by evaluating its prefix using a regular expression.
|
|
||||||
|
|
||||||
Arguments:
|
This function checks if the provided URL adheres to secure HTTPS protocol.
|
||||||
url (str): The URL to evaluate.
|
It uses a regular expression to validate the URL prefix.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url (str): The URL string to validate.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the URL starts with "https://", indicating it may
|
bool: True if the URL starts with "https://", False otherwise.
|
||||||
be considered safe. False otherwise.
|
|
||||||
"""
|
"""
|
||||||
return bool(re.match(r"^https://", url, re.IGNORECASE))
|
return bool(re.match(r"^https://", url, re.IGNORECASE))
|
||||||
|
|
||||||
|
|
||||||
def format_attributes(attributes: str | None = None) -> dict:
|
def format_attributes(attributes: str | None = None) -> dict:
|
||||||
|
"""
|
||||||
|
Parses a string of attributes into a dictionary.
|
||||||
|
|
||||||
|
This function takes a string input representing attributes and their values,
|
||||||
|
formatted as `key=value` pairs separated by commas, and converts it into a
|
||||||
|
dictionary. It returns an empty dictionary if the input is `None` or invalid.
|
||||||
|
Invalid key-value pairs within the input string are skipped.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
attributes (str | None): A comma-separated string of key-value pairs in the
|
||||||
|
format `key=value`, or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary where keys are the attribute names and values are their
|
||||||
|
corresponding values.
|
||||||
|
"""
|
||||||
if not attributes:
|
if not attributes:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
@ -111,6 +175,17 @@ def format_attributes(attributes: str | None = None) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def get_project_parameters() -> dict:
|
def get_project_parameters() -> dict:
|
||||||
|
"""
|
||||||
|
Fetches project parameters from cache or configuration.
|
||||||
|
|
||||||
|
This function retrieves project parameters from a cache if available.
|
||||||
|
If they are not cached, it collects the parameters from a designated
|
||||||
|
configuration source, formats their keys to lowercase, and then stores
|
||||||
|
them in the cache for a limited period.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing the project parameters with lowercase keys.
|
||||||
|
"""
|
||||||
parameters = cache.get("parameters", {})
|
parameters = cache.get("parameters", {})
|
||||||
|
|
||||||
if not parameters:
|
if not parameters:
|
||||||
|
|
@ -123,6 +198,19 @@ def get_project_parameters() -> dict:
|
||||||
|
|
||||||
|
|
||||||
def resolve_translations_for_elasticsearch(instance, field_name) -> None:
|
def resolve_translations_for_elasticsearch(instance, field_name) -> None:
|
||||||
|
"""
|
||||||
|
Resolves translations for a given field in an Elasticsearch-compatible
|
||||||
|
format. It checks if the localized version of the field contains data,
|
||||||
|
and if not, sets it to the value of the default field.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
instance: The object instance containing the field to resolve.
|
||||||
|
field_name (str): The base name of the field for which translations
|
||||||
|
are being resolved.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
field = getattr(instance, f"{field_name}_{LANGUAGE_CODE}", "")
|
field = getattr(instance, f"{field_name}_{LANGUAGE_CODE}", "")
|
||||||
filled_field = getattr(instance, field_name, "")
|
filled_field = getattr(instance, field_name, "")
|
||||||
if not field:
|
if not field:
|
||||||
|
|
@ -134,12 +222,18 @@ CROCKFORD = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||||
|
|
||||||
def generate_human_readable_id(length: int = 6) -> str:
|
def generate_human_readable_id(length: int = 6) -> str:
|
||||||
"""
|
"""
|
||||||
Generate a human-readable ID of `length` characters (from the Crockford set),
|
Generates a human-readable identifier using Crockford's Base32 characters.
|
||||||
with a single hyphen inserted:
|
|
||||||
- 50% chance at the exact middle
|
|
||||||
- 50% chance at a random position between characters (1 to length-1)
|
|
||||||
|
|
||||||
The final string length will be `length + 1` (including the hyphen).
|
This function creates a string of a specified length composed of randomly
|
||||||
|
selected characters from the Crockford Base32 alphabet. A dash is inserted
|
||||||
|
at a random or mid-point position in the identifier for better readability.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
length (int): The length of the identifier excluding the dash. Must be
|
||||||
|
greater than 0. Default is 6.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A human-readable identifier with the specified length plus a dash.
|
||||||
"""
|
"""
|
||||||
chars = [secrets.choice(CROCKFORD) for _ in range(length)]
|
chars = [secrets.choice(CROCKFORD) for _ in range(length)]
|
||||||
|
|
||||||
|
|
@ -151,6 +245,13 @@ def generate_human_readable_id(length: int = 6) -> str:
|
||||||
|
|
||||||
def generate_human_readable_token() -> str:
|
def generate_human_readable_token() -> str:
|
||||||
"""
|
"""
|
||||||
Generate a human-readable token of 20 characters (from the Crockford set),
|
Generates a human-readable token.
|
||||||
|
|
||||||
|
This function creates a random token using characters from
|
||||||
|
the CROCKFORD base32 set. The generated token is 20 characters
|
||||||
|
long and is designed to be human-readable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A 20-character random token.
|
||||||
"""
|
"""
|
||||||
return "".join([secrets.choice(CROCKFORD) for _ in range(20)])
|
return "".join([secrets.choice(CROCKFORD) for _ in range(20)])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue