fabriceb / gcalcron Goto Github PK
View Code? Open in Web Editor NEWSimple multi-platform GUI for CRON: manage your tasks scheduling with Google Calendar !
Simple multi-platform GUI for CRON: manage your tasks scheduling with Google Calendar !
I have a bunch of pis running this that have been working great for years. I just tried setting up a new one and now it doesn't seem like its working any more. Its creating the first entry each time the gcalcron.py gets run even though its a duplicate. I am running the modified version by Mestalbet for python 3, but it had been working fine for months, but now it seems like its not, even on pis that haven't been updated.. The old pis running the python2 version are still ok I think. I can't run the python2 version on the pis anymore since its all been depreciated and removed. Any thoughts?
atq
38 Wed Jul 8 14:00:00 2020 a pi
37 Wed Jul 8 14:00:00 2020 a pi
36 Wed Jul 8 14:00:00 2020 a pi
cat gcalcron/gcalcron.log
Setting up query: 2020-07-08T11:19:23.791570-06:00 to 2020-07-15T11:19:23.791570-06:00 modified after None
Submitting query
Query results received
[{'id': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'status': 'confirmed', 'updated': '2020-07-08T16:39:38.126Z', 'summary': 'Test', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-08T14:00:00-06:00', 'timeZone': 'America/Denver'}, 'end': {'dateTime': '2020-07-08T15:00:00-06:00', 'timeZone': 'America/Denver'}}, {'id': '4pavvn9uosk73i9ok8vf51m4kn', 'status': 'confirmed', 'updated': '2020-07-08T17:17:44.748Z', 'summary': 'test2', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-09T14:30:00-06:00'}, 'end': {'dateTime': '2020-07-09T15:30:00-06:00'}}, {'id': '2lnan2bkthuj9a1jdongv0gtb6', 'status': 'confirmed', 'updated': '2020-07-08T17:17:54.755Z', 'summary': 'Test3', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-10T12:30:00-06:00'}, 'end': {'dateTime': '2020-07-10T13:30:00-06:00'}}]
5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z-confirmed-2020-07-08T16:39:38.126Z: 2020-07-08 14:00:00 -> 2020-07-08 15:00:00 (2020-07-08T14:00:00-06:00 -> 2020-07-08T15:00:00-06:00) =>/home/pi/tvoff.sh
4pavvn9uosk73i9ok8vf51m4kn-confirmed-2020-07-08T17:17:44.748Z: 2020-07-09 14:30:00 -> 2020-07-09 15:30:00 (2020-07-09T14:30:00-06:00 -> 2020-07-09T15:30:00-06:00) =>/home/pi/tvoff.sh
2lnan2bkthuj9a1jdongv0gtb6-confirmed-2020-07-08T17:17:54.755Z: 2020-07-10 12:30:00 -> 2020-07-10 13:30:00 (2020-07-10T12:30:00-06:00 -> 2020-07-10T13:30:00-06:00) =>/home/pi/tvoff.sh
[{'uid': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 8, 14, 0)}]}, {'uid': '4pavvn9uosk73i9ok8vf51m4kn', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 9, 14, 30)}]}, {'uid': '2lnan2bkthuj9a1jdongv0gtb6', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 10, 12, 30)}]}]
['at', '14:00 Jul 08']
b''
b'warning: commands will be executed using /bin/sh\njob 36 at Wed Jul 8 14:00:00 2020\n'
Setting up query: 2020-07-08T11:19:27.928713-06:00 to 2020-07-15T11:19:27.928713-06:00 modified after None
Submitting query
Query results received
[{'id': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'status': 'confirmed', 'updated': '2020-07-08T16:39:38.126Z', 'summary': 'Test', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-08T14:00:00-06:00', 'timeZone': 'America/Denver'}, 'end': {'dateTime': '2020-07-08T15:00:00-06:00', 'timeZone': 'America/Denver'}}, {'id': '4pavvn9uosk73i9ok8vf51m4kn', 'status': 'confirmed', 'updated': '2020-07-08T17:17:44.748Z', 'summary': 'test2', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-09T14:30:00-06:00'}, 'end': {'dateTime': '2020-07-09T15:30:00-06:00'}}, {'id': '2lnan2bkthuj9a1jdongv0gtb6', 'status': 'confirmed', 'updated': '2020-07-08T17:17:54.755Z', 'summary': 'Test3', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-10T12:30:00-06:00'}, 'end': {'dateTime': '2020-07-10T13:30:00-06:00'}}]
5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z-confirmed-2020-07-08T16:39:38.126Z: 2020-07-08 14:00:00 -> 2020-07-08 15:00:00 (2020-07-08T14:00:00-06:00 -> 2020-07-08T15:00:00-06:00) =>/home/pi/tvoff.sh
4pavvn9uosk73i9ok8vf51m4kn-confirmed-2020-07-08T17:17:44.748Z: 2020-07-09 14:30:00 -> 2020-07-09 15:30:00 (2020-07-09T14:30:00-06:00 -> 2020-07-09T15:30:00-06:00) =>/home/pi/tvoff.sh
2lnan2bkthuj9a1jdongv0gtb6-confirmed-2020-07-08T17:17:54.755Z: 2020-07-10 12:30:00 -> 2020-07-10 13:30:00 (2020-07-10T12:30:00-06:00 -> 2020-07-10T13:30:00-06:00) =>/home/pi/tvoff.sh
[{'uid': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 8, 14, 0)}]}, {'uid': '4pavvn9uosk73i9ok8vf51m4kn', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 9, 14, 30)}]}, {'uid': '2lnan2bkthuj9a1jdongv0gtb6', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 10, 12, 30)}]}]
['at', '14:00 Jul 08']
b''
b'warning: commands will be executed using /bin/sh\njob 37 at Wed Jul 8 14:00:00 2020\n'
Setting up query: 2020-07-08T11:19:32.441675-06:00 to 2020-07-15T11:19:32.441675-06:00 modified after None
Submitting query
Query results received
[{'id': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'status': 'confirmed', 'updated': '2020-07-08T16:39:38.126Z', 'summary': 'Test', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-08T14:00:00-06:00', 'timeZone': 'America/Denver'}, 'end': {'dateTime': '2020-07-08T15:00:00-06:00', 'timeZone': 'America/Denver'}}, {'id': '4pavvn9uosk73i9ok8vf51m4kn', 'status': 'confirmed', 'updated': '2020-07-08T17:17:44.748Z', 'summary': 'test2', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-09T14:30:00-06:00'}, 'end': {'dateTime': '2020-07-09T15:30:00-06:00'}}, {'id': '2lnan2bkthuj9a1jdongv0gtb6', 'status': 'confirmed', 'updated': '2020-07-08T17:17:54.755Z', 'summary': 'Test3', 'description': '/home/pi/tvoff.sh', 'start': {'dateTime': '2020-07-10T12:30:00-06:00'}, 'end': {'dateTime': '2020-07-10T13:30:00-06:00'}}]
5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z-confirmed-2020-07-08T16:39:38.126Z: 2020-07-08 14:00:00 -> 2020-07-08 15:00:00 (2020-07-08T14:00:00-06:00 -> 2020-07-08T15:00:00-06:00) =>/home/pi/tvoff.sh
4pavvn9uosk73i9ok8vf51m4kn-confirmed-2020-07-08T17:17:44.748Z: 2020-07-09 14:30:00 -> 2020-07-09 15:30:00 (2020-07-09T14:30:00-06:00 -> 2020-07-09T15:30:00-06:00) =>/home/pi/tvoff.sh
2lnan2bkthuj9a1jdongv0gtb6-confirmed-2020-07-08T17:17:54.755Z: 2020-07-10 12:30:00 -> 2020-07-10 13:30:00 (2020-07-10T12:30:00-06:00 -> 2020-07-10T13:30:00-06:00) =>/home/pi/tvoff.sh
[{'uid': '5e2fnn6ge1d4dsav9692o2i0gg_20200708T200000Z', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 8, 14, 0)}]}, {'uid': '4pavvn9uosk73i9ok8vf51m4kn', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 9, 14, 30)}]}, {'uid': '2lnan2bkthuj9a1jdongv0gtb6', 'commands': [{'command': '/home/pi/tvoff.sh', 'exec_time': datetime.datetime(2020, 7, 10, 12, 30)}]}]
['at', '14:00 Jul 08']
b''
b'warning: commands will be executed using /bin/sh\njob 38 at Wed Jul 8 14:00:00 2020\n'
Here's an idea for a feature that I think would be really useful. It's also something that I would be very happy to write, but I wanted to first hear your thoughts.
I currently have a task that shuts down my desktop computer at 10:30pm on workdays, otherwise I tend to stay up all night working on projects like this...
So in the description of the event, I have the following lines:
-10: DISPLAY=:0 notify-send "Shutting down computer in 10 minutes"
-5: DISPLAY=:0 notify-send "Shutting down computer in 5 minutes"
-3: DISPLAY=:0 notify-send "Shutting down computer in 3 minutes" "Please save your work"
-1: DISPLAY=:0 notify-send Shutting down computer in 1 minute" "Please save your work"
poweroff
I would like to replace this with something like:
macro: shutdown
This would cause the script to search a macros
folder in the application directory, or maybe the users HOME directory, for a file called shutdown
. It would then simply replace that line in the description with the contents of the macro file, and parse the final result.
This means that macros could be combined with commands, or even other macros, to create complex tasks.
What do you think?
ERROR:root:Sync failed
Traceback (most recent call last):
File "./gcalcron2.py", line 427, in main
g.sync_gcal_to_cron()
File "./gcalcron2.py", line 306, in sync_gcal_to_cron
self.schedule_new_jobs(commandsList)
File "./gcalcron2.py", line 262, in schedule_new_jobs
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
File "/usr/lib/python2.7/subprocess.py", line 679, in init
errread, errwrite)
File "/usr/lib/python2.7/subprocess.py", line 1259, in _execute_child
raise child_exception
OSError: [Errno 2] No such file or directory
Rpi python 2.7.3 on raspian
any idea on whats wrong ?
root@RUBY:/gcalcron# python gcalcron.py/gcalcron#
ERROR:root:Sync failed
Traceback (most recent call last):
File "gcalcron.py", line 427, in main
g.sync_gcal_to_cron()
File "gcalcron.py", line 300, in sync_gcal_to_cron
events = self.gCalAdapter.get_events(sync_start, last_sync, num_days)
File "gcalcron.py", line 170, in get_events
return self.queryApi(queries)
File "gcalcron.py", line 137, in queryApi
gCalEvents = self.get_service().events().list(**query).execute()
File "gcalcron.py", line 84, in get_service
credentials = tools.run_flow(FLOW, storage, Flags)
NameError: global name 'flags' is not defined
root@RUBY:
I have had this issue as-well as the initial last_sync being null and throwing out allot of errors, I placed yesterdays date in for a initial run and received this
Found this script ideal for my Raspberry Pi. However, with a default install of Raspbian, i could get the script to download events (as was evident in the log) but the commands would not execute at the scheduled time.
The reason is pretty simple; out of the box, the unix tool "at" is not installed.
To do this, simply run sudo apt-get install at
That should do the trick. I spent a good few hours trying to work this out.
Any idea why it won't work for me, I use Python 2.7 and I believe I have all the required libraires:
python -m doctest -v gcalcron.py
Trying:
g = GCalAdapter()
Expecting nothing
ok
Trying:
g.get_query(datetime.datetime(2011, 6, 19, 14, 0), datetime.datetime(2011, 6, 26, 14, 0), datetime.datetime(2011, 6, 18, 14, 0))
Expecting:
{'orderBy': 'updated', 'showDeleted': True, 'calendarId': None, 'timeMin': '2011-06-19T14:00:00', 'updatedMin': '2011-06-18T14:00:00', 'timeMax': '2011-06-26T14:00:00', 'fields': 'items(description,end,id,start,status,summary,updated)', 'singleEvents': True, 'maxResults': 1000}
ok
Trying:
datetime_to_at(datetime.datetime(2011, 6, 18, 12, 0))
Expecting:
'12:00 Jun 18'
ok
Trying:
parse_commands("echo 'Wake up!'\n+10: echo 'Wake up, you are 10 minutes late!'", datetime.datetime(3011, 6, 19, 8, 30), datetime.datetime(3011, 6, 19, 9, 0))
Expecting:
[{'exec_time': datetime.datetime(3011, 6, 19, 8, 30), 'command': "echo 'Wake up!'"}, {'exec_time': datetime.datetime(3011, 6, 19, 8, 40), 'command': "echo 'Wake up, you are 10 minutes late!'"}]
ok
Trying:
parse_commands("Turn on lights\nend -10: Dim lights\nend: Turn off lights", datetime.datetime(3011, 6, 19, 18, 30), datetime.datetime(3011, 6, 19, 23, 0))
Expecting:
[{'exec_time': datetime.datetime(3011, 6, 19, 18, 30), 'command': 'Turn on lights'}, {'exec_time': datetime.datetime(3011, 6, 19, 22, 50), 'command': 'Dim lights'}, {'exec_time': datetime.datetime(3011, 6, 19, 23, 0), 'command': 'Turn off lights'}]
ok
Trying:
parse_events([{u'status': u'confirmed', u'updated': u'2013-12-22T19:49:13.750Z', u'end': {u'dateTime': u'2013-12-23T02:00:00+01:00'}, u'description': u'-60: start_heating.py\n0: turn_music_on.py\n+30: stop_heating.py', u'summary': u'Wakeup', u'start': {u'dateTime': u'2013-12-23T01:00:00+01:00'}, u'id': u'olbia2urfm1ns0h88v4u0d9a5g'}])
Expecting:
[{'commands': [{'exec_time': datetime.datetime(2013, 12, 23, 0, 0), 'command': u'start_heating.py'}, {'exec_time': datetime.datetime(2013, 12, 23, 1, 0), 'command': u'0: turn_music_on.py'}, {'exec_time': datetime.datetime(2013, 12, 23, 1, 30), 'command': u'stop_heating.py'}], 'uid': u'olbia2urfm1ns0h88v4u0d9a5g'}]
File "gcalcron.py", line 358, in gcalcron.parse_events
Failed example:
parse_events([{u'status': u'confirmed', u'updated': u'2013-12-22T19:49:13.750Z', u'end': {u'dateTime': u'2013-12-23T02:00:00+01:00'}, u'description': u'-60: start_heating.py\n0: turn_music_on.py\n+30: stop_heating.py', u'summary': u'Wakeup', u'start': {u'dateTime': u'2013-12-23T01:00:00+01:00'}, u'id': u'olbia2urfm1ns0h88v4u0d9a5g'}])
Expected:
[{'commands': [{'exec_time': datetime.datetime(2013, 12, 23, 0, 0), 'command': u'start_heating.py'}, {'exec_time': datetime.datetime(2013, 12, 23, 1, 0), 'command': u'0: turn_music_on.py'}, {'exec_time': datetime.datetime(2013, 12, 23, 1, 30), 'command': u'stop_heating.py'}], 'uid': u'olbia2urfm1ns0h88v4u0d9a5g'}]
Got:
[]
Trying:
parse_events([{u'status': u'cancelled', u'updated': u'2013-12-22T19:52:50.525Z', u'end': {u'dateTime': u'2013-12-23T02:00:00+01:00'}, u'description': u'-60: start_heating.py\n0: turn_music_on.py\n+30: stop_heating.py', u'summary': u'Wakeup', u'start': {u'dateTime': u'2013-12-23T01:00:00+01:00'}, u'id': u'olbia2urfm1ns0h88v4u0d9a5g'}])
Expecting:
[{'uid': u'olbia2urfm1ns0h88v4u0d9a5g'}]
ok
17 items had no tests:
gcalcron
gcalcron.GCalAdapter
gcalcron.GCalAdapter.init
gcalcron.GCalAdapter.get_events
gcalcron.GCalAdapter.queryApi
gcalcron.GCalCron
gcalcron.GCalCron.init
gcalcron.GCalCron.clean_settings
gcalcron.GCalCron.getCalendarId
gcalcron.GCalCron.init_settings
gcalcron.GCalCron.load_settings
gcalcron.GCalCron.reset_settings
gcalcron.GCalCron.save_settings
gcalcron.GCalCron.schedule_new_jobs
gcalcron.GCalCron.sync_gcal_to_cron
gcalcron.GCalCron.unschedule_old_jobs
gcalcron.main
3 items passed all tests:
2 tests in gcalcron.GCalAdapter.get_query
1 tests in gcalcron.datetime_to_at
2 tests in gcalcron.parse_commands
1 items had failures:
1 of 2 in gcalcron.parse_events
7 tests in 21 items.
6 passed and 1 failed.
_Test Failed_ 1 failures.
root@ubuntu:~/gcalcron# python gcalcron.py
ERROR:root:Sync failed
Traceback (most recent call last):
File "gcalcron.py", line 420, in main
g.sync_gcal_to_cron()
File "gcalcron.py", line 297, in sync_gcal_to_cron
commandsList = parse_events(events)
File "gcalcron.py", line 371, in parse_events
logger.debug(event['id'] + '-' + event['status'] + '-' + event['updated'] + ': ' + unicode(start_time) + ' -> ' + unicode(end_time) + ' (' + event['start']['dateTime'] + ' -> ' + event['end']['dateTime'] + ') ' + '=>' + event['description'])
KeyError: 'description'
First, thanks so much for creating this! You have done an awesome job, and it works almost perfectly! However, I managed to find a small bug:
-10: DISPLAY=:0 notify-send "The task will run in 10 minutes"
python gcalcron2.py
at 8:25 pm.The script crashes like this:
Traceback (most recent call last):
File "/home/ndbroadbent/src/python/GCalCron2/gcalcron2.py", line 297, in <module>
g.sync_gcal_to_cron()
File "/home/ndbroadbent/src/python/GCalCron2/gcalcron2.py", line 246, in sync_gcal_to_cron
job_id = re.compile('job (\d+) at').search(output).group(1)
AttributeError: 'NoneType' object has no attribute 'group'
Thanks again for your sharing your work!
I can't wait to use this for my home automation schedule, as well as Wake-on-Lan & shutdown for my PCs.
Here's another thing that I have been thinking about for a long time. When I'm on holiday, I would love to be able to simply add the event to my 'Holidays' calendar, and have all of my alarms and work-related automation tasks automatically disabled.
So, for example, my home automation system interacts with XBMC, and turns on music in the morning. I also automate a WakeOnLan packet to my computers at the office to turn them on in the morning. I'd also like to automate the heaters and air-conditioning, so that I don't waste energy during the day while I'm at work.
So instead of manually cancelling all these events, it would be great if all we had to do was create a 'holiday' event that automatically disabled them. I already subscribe to a calendar of public holidays, so it would amazing if my alarm automatically turned itself off when I don't need to go to work. You mentioned that a cancelled task woke up your sister during a holiday, so I guess you know what I mean...
I don't think Google Calendar itself can handle these kinds of rules, but please let me know if it can.
I would love to also write a translation layer between the 'at' command, and the alarm app of my (jailbroken) iPhone, so that I can use GCalCron2 to manage my iPhone alarms. (I don't want to use the default calendar alerts because they're too quiet, can't customize ringtone, don't have snooze, etc.)
So, you can see why I'm excited about this feature :)
Maybe this idea is out of the scope of GCalCron2, but I was wondering what you think?
I've finished this feature, but didn't make a pull request for it yet, because it requires some other pull requests to be accepted first.
As well as +10:
, -10:
, you can now also write end:
, end +10:
, end -10:
.
You can see the change here: ndbroadbent@9b14d5b
It's on my support_commands_at_end branch, along with the changes to the README.
Hi,
Awesome package! Love the idea.
I tried following your instructions in the readme.md but when I run the script - it looks for a file not created yet. Also, not sure where to give it the calendar ID.
Cheers,
Noah
pi@RUBY ~/gcalcron $ python gcalcron.py
ERROR:root:Sync failed
Traceback (most recent call last):
File "gcalcron.py", line 420, in main
g.sync_gcal_to_cron()
File "gcalcron.py", line 296, in sync_gcal_to_cron
events = self.gCalAdapter.get_events(sync_start, last_sync, num_days)
File "gcalcron.py", line 166, in get_events
return self.queryApi(queries)
File "gcalcron.py", line 134, in queryApi
gCalEvents = self.service.events().list(*_query).execute()
File "/usr/local/lib/python2.7/dist-packages/oauth2client/util.py", line 132, in positional_wrapper
return wrapped(_args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/apiclient/http.py", line 723, in execute
raise HttpError(resp, content, uri=self.uri)
HttpError: <HttpError 404 when requesting https://www.googleapis.com/calendar/v3/calendars/%[email protected]/events?orderBy=updated&timeMax=2014-01-22T22%3A27%3A31.942439%2B00%3A00&fields=items%28description%2Cend%2Cid%2Cstart%2Cstatus%2Csummary%2Cupdated%29&singleEvents=true&maxResults=1000&showDeleted=true&timeMin=2014-01-15T22%3A27%3A31.942439%2B00%3A00&alt=json returned "Not Found">
With DEBUG = True
:
# python /home/ndbroadbent/src/python/GCalCron2/gcalcron2.py
Setting up query: 2012-01-08 21:24:02.781849 to 2012-01-15 21:23:00 modified after 2012-01-08 21:23:00
Setting up query: 2012-01-15 21:23:00 to 2012-01-15 21:24:02.781849 modified after None
Submitting query
Query results received
http://www.google.com/calendar/feeds/[email protected]/private/full/sf20hslp7r3jtmkgllm3r0tcl4 - CANCELED - 2012-01-08T13:23:49.000Z : Do this thing 2012-01-12 10:00:00 ( 2012-01-12T02:00:00.000Z ) => dagsdafsfda
[{'commands': [{'exec_time': datetime.datetime(2012, 1, 12, 10, 0), 'command': 'dagsdafsfda'}], 'uid': 'http://www.google.com/calendar/feeds/[email protected]/private/full/sf20hslp7r3jtmkgllm3r0tcl4'}]
at -d 287
at 10:00 Jan 12
warning: commands will be executed using /bin/sh
job 288 at Thu Jan 12 10:00:00 2012
So it removes the cancelled event (at -d 287
), but re-adds it again (at 10:00 Jan 12
)
Would you be in favor of replacing import datetime
with from datetime import datetime, timedelta
?
We could then use just datetime
and timedelta
instead of datetime.datetime
and datetime.timedelta
.
Thanks for merging my requests, by the way!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.