checking when a command was last used - python

I have a discord bot which has a command that requires a cooldown of a week. I considered using something like #commands.cooldown, but the issue is that if the bot goes offline, that cooldown resets itself. So, a member could simply watch for when the bot goes down and try to exploit it after it returns.
Since I can't keep the bot running 24x7x365, the solution I came up with was to record the date a user last used the command in a database, and when they use the command next, to check if seven days have passed.
For example's sake, this is what the command would look like:
#bot.command()
async def datetest(ctx,date):
today = datetime.date.today()
print(today)
someday = datetime.date.strptime(date,"%y-%m-%d")
diff = someday - today
await ctx.send(diff + "days.")
In this example, I'm using "date" as a parameter to just test if the command returns the difference of days.
(in the actual command, rather than take a date argument, it'll get a date stored in the DB)
However, the issue I'm facing is that:
someday = datetime.date.strptime(date,"%y-%m-%d")
doesn't run. What am I doing wrong here?

1: To format a date it's strftime 'f' for 'format' not strptime, 'parse'. strptime is used to turn a string object into a datetime object.
2: If you are working with the object you don't need to also pass that object to the methods of that class. i.e. you don't need to pass date to the object method 'strftime'
This is because, as with all object methods, it's done internally using the implicit 'self' parameter.
3: To get the actual date object you need to call a method of the datetime class which produces that. e.g. datetime.now()
Your code line should simply read something like:
import datetime.datetime as datetime
# or from datetime import datetime
someday = datetime.now().date().strftime("%y-%m-%d")
or
import datetime
someday = datetime.datetime.now().date().strftime("%y-%m-%d")
(Reading your post not sure what your intent is, so adding this for completeness)
Going the other way, getting a value from your database and using that to generate a datetime.
I don't know what database interface you are using so this is a general answer, as for many Python DB interfaces this is not necessary as the date field select() call will return a type for that field that is a datetime object.
# Paraphrasing a DB interface
dt_col = db.select("select max(date) from lastrun_dates;").execute()
# returns ["2022-10-3",]
date = datetime.datetime.strptime(dt_col[0], "%Y-m-%d")
# note the uppercase 'Y' for a 4 digit date
However, don't do this!
I don't recommend saving dates/times in a DB (or anywhere else for that matter) as a string as it has a DB schema, geographical, localle and daylight saving interpretation, which results in many ways of creating future issues. Instead it is recommended to use the UTC datetime from the epoch as an integer or float.
datetime.datetime.utcnow().timestamp()
# or
datetime.datetime.utcnow().toordinal()

Ok, so I figured out the issue after digging deeper into how datetime subtractions work.
One is that I needed to use someday = datetime.datetime.strptime().
Two is that I needed to use %Y instead of %y to match the time format passed into the date parameter (yyyy-mm-dd). Console pointed this out after a few tests.
Third is that I needed to extract the number of days out of the timedelta answer, because in the end, I'm trying to see how many days passed from the time the command was last used.
So, the new code looks like:
someday = datetime.datetime.strptime(date, "%Y-%m-%d")
diff = someday - today
print(diff.days)

Related

How to properly implement method cascading - chaining in Python?

I have a python code roughly like this:
class SomeClass():
def get_date(self):
date = # read the data from the database in YYYY-MM-DD HH:MM:SS format
return date
now, I will use the returned value in an API query, which accepts the input as a unix timestamp. Actually I can give the returned date to another function to have it converted to a timestamp, but I thought it would be a good opportunity to learn about function cascading / chaining, which I've always wondered about in Python.
Now, what I basically want to achieve is something like this:
If I just execute SomeClass.get_date(), I want to have the date in the same format that has been returned from the database, which is YYYY-MM-DD HH:MM:SS
If I execute SomeClass.get_date().as_timestamp(), I should get it as timestamp.
If not possible, I would settle for having it like SomeClass.get_date().as_datetime() and SomeClass.get_date().as_timestamp()
I might use the second function (.as_timestamp()) for more than one primary function (there are multiple datetime columns that I may need to be converted into timestamp).
I've looked into some examples of function cascading. They are mostly saying that key to this is returning the self, but I could not find any info about how to implement it in the way I need to.
I basically need the return value to be fed into the second function (I guess), but not sure if it is possible, and don't know how to do it. Any ideas?
What you're looking for looks more like this:
import time
import calendar
class DateClass():
def get_date(self):
self = # read the data from the database in YYYY-MM-DD HH:MM:SS format
return self
def as_timestamp(self):
mysql_time = time.strptime(self, '%Y-%m-%d %H:%M:%S')
self = calendar.timegm(mysql_time)
return self
d = DateClass()
print(d.get_date())
print(d.get_date().as_timestamp())
print(d)
d.get_date()
print(d)
The first output would be the MySQL datetime, and the second would be the Unix timestamp, the third would also be the unix timestamp and the fourth would be the datetime. get_date() and as_timestamp() mutate the instance d when called on it.
In this configuration, as_timestamp() requires get_date(), which seems to be in line with your question, but you could change that by assigning self from the database in as_timestamp() like this:
def as_timestamp(self):
self = # read the data from the database in YYYY-MM-DD HH:MM:SS format
mysql_time = time.strptime(self, '%Y-%m-%d %H:%M:%S')
self = calendar.timegm(mysql_time)
return self
which will make it possible to call as_timestamp() independent of get_date(). You can also then add d.as_timestamp() to mutate any iteration of d so that d is a timestamp.

sqlalchemy: Call STR_TO_DATE on column

I am moving some of my code onto sqlalchemy from using raw MySQL queries.
The current issue I am having is that the datetime was saved in a string format by a C# tool. Unfortunately, the representation does not match up with Python's (as well as that it has an extra set of single quotes), thus making filtering somewhat cumbersome.
Here is an example of the format that the date was saved in:
'2016-07-01T17:27:01'
Which I was able to convert to a usable datetime using the following MySQL command:
STR_TO_DATE(T.PredicationGeneratedTime, \"'%%Y-%%m-%%dT%%H:%%i:%%s'\")
However, I cannot find any documentation that describes how to invoke built-in functions such as STR_TO_DATE when filtering with sqlalchemy
The following Python code:
session.query(Train.Model).filter(cast(Train.Model.PredicationGeneratedTime, date) < start)
is giving me:
TypeError: Required argument 'year' (pos 1) not found
There does not seem to be a way to specify the format for the conversion.
Note: I realize the solution is to fix the way the datetime is stored, but in the mean time I'd like to run queries against the existing data.
You can try to use func.str_to_date(COLUMN, FORMAT_STRING) instead of cast
In the cast() you should be using sqlalchemy.DateTime, not (what I assume is) a datetime.date - that is the cause of the exception.
However, fixing that will not really help because of the embedded single quotes.
You are fortunate that the dates stored in your table are in ISO format. That means that lexicographic comparisons will work on the date strings themselves, without casting. As long as you use a string for start with the surrounding single quotes, it will work.
from datetime import datetime
start = "'{}'".format(datetime.now().isoformat())
session.query(Train.Model).filter(Train.Model.PredicationGeneratedTime < start)

Django + Postgres Timezones

I'm trying to figure out what's going on with a timezone conversion that's happening in Django.
My view code is as below, it filters on a date range and groups on the day of creation:
def stats_ad(request):
start_date = datetime.datetime.strptime(request.GET.get('start'), '%d/%m/%Y %H:%M:%S')
end_date = datetime.datetime.strptime(request.GET.get('end'), '%d/%m/%Y %H:%M:%S')
fads = Ad.objects.filter(created__range=[start_date, end_date]).extra(select={'created_date': 'created::date'}).values('created_date').annotate(total=Count('id')).order_by("created_date")
The SQL query that is produced by django when I set the get variable of start to "01/05/2013 00:00:00" and the request end variable to "11/05/2013 23:59:00":
SELECT (created::date) AS "created_date", COUNT("ads_ad"."id") AS "total" FROM "ads_ad" WHERE "ads_ad"."created" BETWEEN E'2013-05-01 00:00:00+10:00' and E'2013-05-11 23:59:59+10:00' GROUP BY created::date, (created::date) ORDER BY "created_date" ASC
If I manually run that on my Postgresql database, it's all good, finds the following:
created_date total
2013-05-10 22
2013-05-11 1
However If I do the following:
for a in fads:
recent_ads.append({"dates": a['created_date'].strftime('%d/%m/%Y'), 'ads': a['total']})
It gives me the following output:
[{"dates": "09/05/2013", "ads": 1}, {"dates": "10/05/2013", "ads": 22}]
I'm at a loss at why it's changed the dates?
Anyone got any ideas?
Cheers,
Ben
Just a through on this. As of Django 1.4, Django now supports timezone aware dates and times. Perhaps it's possible that a conversion between your local timezone and the timezone that the data is stored in (possibly GMT) is taking place at some point. Perhaps that difference crosses the international date line, in which case the dates may show up differently.
Django has an interesting section describing the new timezone support feature.
https://docs.djangoproject.com/en/1.4/topics/i18n/timezones/
That's what came to mind, anyway, when you described your problem. Hope this helps.
Python datetime from the standard python library is a mess.
Probably you are creating naive datetime instances (instances that lack timezone information).
# naive
now = datetime.datetime.now()
# TZ aware
from django.utils.timezone import utc
now = datetime.datetime.utcnow().replace(tzinfo=utc)
In recent Django, datetime storage is always offset-aware, so you better convert naive datetimes - otherwise an automatic (and sometimes wrong) conversion will take place.
Take a look at the docs about Django Time Zones.

In Python, how is `datetime.datetime.now()` an "unaware" datetime object?

Apparently, I must run pytz.UTC.localize(datetime.datetime.now()) before I compare the current time with other "naive" time objects.
Why is that? Isn't the current time relevant to specific time zones?
>>> datetime.datetime.now().utcoffset() is None
True
The now function does indeed return a naive object. It contains field values for day and time that are populated according to locale settings. That is why a separate utcnow is provided; that always gives you UTC field values (though it is still naive).

How to deal with "partial" dates (2010-00-00) from MySQL in Django?

In one of my Django projects that use MySQL as the database, I need to have a date fields that accept also "partial" dates like only year (YYYY) and year and month (YYYY-MM) plus normal date (YYYY-MM-DD).
The date field in MySQL can deal with that by accepting 00 for the month and the day. So 2010-00-00 is valid in MySQL and it represent 2010. Same thing for 2010-05-00 that represent May 2010.
So I started to create a PartialDateField to support this feature. But I hit a wall because, by default, and Django use the default, MySQLdb, the python driver to MySQL, return a datetime.date object for a date field AND datetime.date() support only real date. So it's possible to modify the converter for the date field used by MySQLdb and return only a string in this format 'YYYY-MM-DD'. Unfortunately the converter use by MySQLdb is set at the connection level so it's use for all MySQL date fields. But Django DateField rely on the fact that the database return a datetime.date object, so if I change the converter to return a string, Django is not happy at all.
Someone have an idea or advice to solve this problem? How to create a PartialDateField in Django ?
EDIT
Also I should add that I already thought of 2 solutions, create 3 integer fields for year, month and day (as mention by Alison R.) or use a varchar field to keep date as string in this format YYYY-MM-DD.
But in both solutions, if I'm not wrong, I will loose the special properties of a date field like doing query of this kind on them: Get all entries after this date. I can probably re-implement this functionality on the client side but that will not be a valid solution in my case because the database can be query from other systems (mysql client, MS Access, etc.)
First, thanks for all your answers. None of them, as is, was a good solution for my problem, but, for your defense, I should add that I didn't give all the requirements. But each one help me think about my problem and some of your ideas are part of my final solution.
So my final solution, on the DB side, is to use a varchar field (limited to 10 chars) and storing the date in it, as a string, in the ISO format (YYYY-MM-DD) with 00 for month and day when there's no month and/or day (like a date field in MySQL). This way, this field can work with any databases, the data can be read, understand and edited directly and easily by a human using a simple client (like mysql client, phpmyadmin, etc.). That was a requirement. It can also be exported to Excel/CSV without any conversion, etc. The disadvantage is that the format is not enforce (except in Django). Someone could write 'not a date' or do a mistake in the format and the DB will accept it (if you have an idea about this problem...).
This way it's also possible to do all of the special queries of a date field relatively easily. For queries with WHERE: <, >, <=, >= and = work directly. The IN and BETWEEN queries work directly also. For querying by day or month you just have to do it with EXTRACT (DAY|MONTH ...). Ordering work also directly. So I think it covers all the query needs and with mostly no complication.
On the Django side, I did 2 things. First, I have created a PartialDate object that look mostly like datetime.date but supporting date without month and/or day. Inside this object I use a datetime.datetime object to keep the date. I'm using the hours and minutes as flag that tell if the month and day are valid when they are set to 1. It's the same idea that steveha propose but with a different implementation (and only on the client side). Using a datetime.datetime object gives me a lot of nice features for working with dates (validation, comparaison, etc.).
Secondly, I have created a PartialDateField that mostly deal with the conversion between the PartialDate object and the database.
So far, it works pretty well (I have mostly finish my extensive unit tests).
You could store the partial date as an integer (preferably in a field named for the portion of the date you are storing, such as year, month or day) and do validation and conversion to a date object in the model.
EDIT
If you need real date functionality, you probably need real, not partial, dates. For instance, does "get everything after 2010-0-0" return dates inclusive of 2010 or only dates in 2011 and beyond? The same goes for your other example of May 2010. The ways in which different languages/clients deal with partial dates (if they support them at all) are likely to be highly idiosyncratic, and they are unlikely to match MySQL's implementation.
On the other hand, if you store a year integer such as 2010, it is easy to ask the database for "all records with year > 2010" and understand exactly what the result should be, from any client, on any platform. You can even combine this approach for more complicated dates/queries, such as "all records with year > 2010 AND month > 5".
SECOND EDIT
Your only other (and perhaps best) option is to store truly valid dates and come up with a convention in your application for what they mean. A DATETIME field named like date_month could have a value of 2010-05-01, but you would treat that as representing all dates in May, 2010. You would need to accommodate this when programming. If you had date_month in Python as a datetime object, you would need to call a function like date_month.end_of_month() to query dates following that month. (That is pseudocode, but could be easily implemented with something like the calendar module.)
It sounds like you want to store a date interval. In Python this would (to my still-somewhat-noob understanding) most readily be implemented by storing two datetime.datetime objects, one specifying the start of the date range and the other specifying the end. In a manner similar to that used to specify list slices, the endpoint would not itself be included in the date range.
For example, this code would implement a date range as a named tuple:
>>> from datetime import datetime
>>> from collections import namedtuple
>>> DateRange = namedtuple('DateRange', 'start end')
>>> the_year_2010 = DateRange(datetime(2010, 1, 1), datetime(2011, 1, 1))
>>> the_year_2010.start <= datetime(2010, 4, 20) < the_year_2010.end
True
>>> the_year_2010.start <= datetime(2009, 12, 31) < the_year_2010.end
False
>>> the_year_2010.start <= datetime(2011, 1, 1) < the_year_2010.end
False
Or even add some magic:
>>> DateRange.__contains__ = lambda self, x: self.start <= x < self.end
>>> datetime(2010, 4, 20) in the_year_2010
True
>>> datetime(2011, 4, 20) in the_year_2010
False
This is such a useful concept that I'm pretty sure that somebody has already made an implementation available. For example, a quick glance suggests that the relativedate class from the dateutil package will do this, and more expressively, by allowing a 'years' keyword argument to be passed to the constructor.
However, mapping such an object into database fields is somewhat more complicated, so you might be better off implementing it simply by just pulling both fields separately and then combining them. I guess this depends on the DB framework; I'm not very familiar with that aspect of Python yet.
In any case, I think the key is to think of a "partial date" as a range rather than as a simple value.
edit
It's tempting, but I think inappropriate, to add more magic methods that will handle uses of the > and < operators. There's a bit of ambiguity there: does a date that's "greater than" a given range occur after the range's end, or after its beginning? It initially seems appropriate to use <= to indicate that the date on the right-hand side of the equation is after the start of the range, and < to indicate that it's after the end.
However, this implies equality between the range and a date within the range, which is incorrect, since it implies that the month of May, 2010 is equal to the year 2010, because May the 4th, 2010 equates to the both of them. IE you would end up with falsisms like 2010-04-20 == 2010 == 2010-05-04 being true.
So probably it would be better to implement a method like isafterstart to explicitly check if a date is after the beginning of the range. But again, somebody's probably already done it, so it's probably worth a look on pypi to see what's considered production-ready. This is indicated by the presence of "Development Status :: 5 - Production/Stable" in the "Categories" section of a given module's pypi page. Note that not all modules have been given a development status.
Or you could just keep it simple, and using the basic namedtuple implementation, explicitly check
>>> datetime(2012, 12, 21) >= the_year_2010.start
True
Can you store the date together with a flag that tells how much of the date is valid?
Something like this:
YEAR_VALID = 0x04
MONTH_VALID = 0x02
DAY_VALID = 0x01
Y_VALID = YEAR_VALID
YM_VALID = YEAR_VALID | MONTH_VALID
YMD_VALID = YEAR_VALID | MONTH_VALID | DAY_VALID
Then, if you have a date like 2010-00-00, convert that to 2010-01-01 and set the flag to Y_VALID. If you have a date like 2010-06-00, convert that to 2010-06-01 and set the flag to YM_VALID.
So, then, PartialDateField would be a class that bundles together a date and the date-valid flag described above.
P.S. You don't actually need to use the flags the way I showed it; that's the old C programmer in me coming to the surface. You could use Y_VALID, YM_VALID, YMD_VALID = range(3) and it would work about as well. The key is to have some kind of flag that tells you how much of the date to trust.
Although not in Python - here's an example of how the same problem was solved in Ruby - using a single Integer value - and bitwise operators to store year, month and day - with month and day optional.
https://github.com/58bits/partial-date
Look at the source in lib for date.rb and bits.rb.
I'm sure a similar solution could be written in Python.
To persist the date (sortable) you just save the Integer to the database.

Categories