2023-07-20
This is intended as a living post that I will update as I find/write utility functions that I commonly reach out for.
Last updated: 2023-07-20
This is useful for running side-effects that would otherwise need you to grant someone a lot of DocType
permissions. It is important that the side-effects are relatively benign as this grants the caller Admin privileges for your function.
import frappe
from frappe.utils.background_jobs import (
get_queue,
execute_job,
RQ_JOB_FAILURE_TTL,
RQ_RESULTS_TTL,
)
from frappe.utils import cstr
def enqueue_as_admin(method, **kwargs, job_name=None, queue_name='long', timeout=3600):
queue_args = {
"site": frappe.local.site,
"user": "Administrator",
"method": method,
"event": None,
"job_name": job_name or cstr(method),
"is_async": True,
"kwargs": kwargs,
}
q = get_queue(queue_name)
j = q.enqueue_call(
execute_job,
timeout=timeout,
kwargs=queue_args,
at_front=False,
failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
)
Converts a file_url
, e.g. from an Attach
field in a document, into a path that can be passed to open
.
import frappe
import os
def get_file_path(file_url):
if not file_url:
return None
site_dir = frappe.get_site_path()
if file_url.startswith("/"):
file_url = file_url[1:]
return os.path.join(site_dir, file_url)
Given a date, returns a number in the range 1-5.
from datetime import timedelta
def week_of_month(date):
month = date.month
week = 0
while date.month == month:
week += 1
date -= timedelta(days=7)
if week > 4:
week = 4
return week
Given a month, year and week within the month, returns the day (number) of the first day of the week.
from datetime import datetime, timedelta
def get_first_day_of_week(year, month, week):
"""Returns the first day of the week."""
date = datetime.strptime("{}-{}-1".format(year, month), "%Y-%m-%d")
first_day_of_week = date + timedelta(days=(week - 1) * 7)
return first_day_of_week
I have many variants of this parser which handle different kinds of datetime
formats and either return None
or raise an error as needed in that context.
from datetime import datetime
def parse_date(row_date):
formats = [
"%d-%m-%Y",
"%Y-%m-%d",
"%d/%m/%Y",
]
for format in formats:
try:
return datetime.strptime(row_date, format).date()
except:
pass
raise ValueError("Invalid date format for Date", row_date)
There's also frappe.generate_hash
that's a bit more opinionated. This function allows you to specify the characters to pick from. I use this to generate numeric OTPs by calling random_string_generator(4, string.digits)
import random
def random_string_generator(str_size, allowed_chars):
return "".join(random.choice(allowed_chars) for x in range(str_size))
json.dumps
fails on dictionaries with datetime
values. This fixes it. As needed, I will sometimes add additional handlers into this function. See also frappe.as_json
.
def date_json_serial(obj):
if isinstance(obj, (datetime, date)):
return obj.date().isoformat()
raise TypeError("Type %s not serializable" % type(obj))
math.round
has some weird behaviour. This function implements round
the way you and I understand it.
def normal_round(num, ndigits=0):
"""
Rounds a float to the specified number of decimal places.
num: the value to round
ndigits: the number of digits to round to
From: https://medium.com/thefloatingpoint/pythons-round-function-doesn-t-do-what-you-think-71765cfa86a8
"""
if ndigits == 0:
return int(num + 0.5)
else:
digit_value = 10**ndigits
return int(num * digit_value + 0.5) / digit_value
Dictionary keys do not need to be strings - they can be lists, tuples etc. This is incredibly useful to track groups by categories. If you are currently creating keys that look like f"{category_1}-{category_2}"
, you could be doing this: (category_1, category_2)
.
But can you use a dictionary as a key for another dictionary? No. Think that's crazy? Maybe, but here's how you would do it - by converting the dictionary into a tuple of key, value pairs.
def dict_to_hashable_key(d):
def to_hashable(value):
if isinstance(value, list):
return tuple(to_hashable(item) for item in value)
return value
return tuple(sorted((k, to_hashable(v)) for k, v in d.items()))