{"action":"created","comment":{"url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/comments/236548691","pull_request_review_id":178629174,"id":236548691,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIzNjU0ODY5MQ==","diff_hunk":"@@ -0,0 +1,292 @@\n+:Author: Jean Laroche <foo@foo.com>\r\n+\r\n+================================================================================\r\n+ Pythran as a bridge between fast prototyping and code deployment\r\n+================================================================================\r\n+\r\n+\r\n+Introduction\r\n+================================================================================\r\n+\r\n+As a researcher/engineer (really, an algorithm developer) in the general area of audio and speech processing, I've always run into the same difficulty at all the companies I've worked at: How to quickly prototype and develop algorithms, and subsequently turn them into efficient code that can be deployed to customers. These have always been incompatible goals: For rapid prototyping, over many years, I've used matlab, then python with numpy. Both are wonderful in the level of mathematical abstraction they provide (for example, 1 line to multiply a matrix by a vector), and they're very useful for testing and debugging as they're interpreted and provide a full array of plotting routines for inspecting what's going on in your algorithm. For example, you can put a breakpoint in your program, and when it stops, you can start poking around, executing high-level commands, etc. But when came time to deploy the prototype algorithm, we typically had to byte the bullet and rewrite it in C or C++: For one thing, it was not possible or practical to ship matlab or numpy to our customers so our prototype code could be run in their environment. It was also crucial to squeeze every drop of efficiency out of the processor, and predictibility and reproducibility of performance was also very important! When processing real-time audio, you cannot wait around for garbage collection to terminate because you will run out of audio samples to play. So interpreted languages were pretty much out. This means we had teams of people porting the matlab or python code to C++, creating test vectors to ensure results were similar enough, and henceforth maintaining the C++ code to ensure that it still matched the matlab/python reference code. This was an enormous overhead, but one that was still preferable to prototyping in C/C++. In some of the companies I worked at, we tested various tools to convert matlab to C++ but none of them were truly satisfactory (most of them required you to severely constrain the way you coded so the conversion code could run).\r\n+\r\n+Then I discovered pythran. Unlike Cython and numba, pythran not only accelerates your python code (by compiling modules into fast .so files) but pythran also generates self-contained C++ code that implements your python/numpy algorithm. The C++ code is fully portable, does not require any python or numpy libraries, does not rely at all on python, and can easily be incorporated into a C++ project. Once compiled, you prototype code becomes extremely efficient, optimized for the target architecture, and its performance is predictible and reproducible. What's more, you do not need to change anything to your python code to make it work with pythran, provided that you're using supported functions and features, so you can keep your prototype code as your reference, update and improve it, and re-generate the fast C++ version with a single command. No need to maintain and synchronize two versions of the same code.\r\n+To me, this is nothing short of a revolution in how I do my work. To show you the new workflow I use, I've take an example of a moderately complicated audio algorithm written in python. With this blog, you'll be able to see how simple it is to convert the high-level python/numpy code into a fully deployable, fast, C++ version.\r\n+\r\n+\r\n+An example: time-scaling an audio file.\r\n+================================================================================\r\n+\r\n+A note on time-scaling:\r\n+__________________________________\r\n+\r\n+\r\n+Time scaling in audio signal processing refers to slowing down or speeding up an audio recording while preserving its original pitch (frequency). You may be familiar with playing back a record or a tape at half its normal speed: this both slows down the music but it also changes the pitch: for example a singer's voice become completely unnatural, a female singer might start sounding more like a male. Instead of the music being in a certain key, say A major, it will play in a different key (E major for example, if speeding up by a factor 1.5x. This is undesirable and many techniques have been developed to allow controlling the playback speed while preserving the original \"tone\" of the music.\r\n+Time-scaling is useful for example if you're trying to figure out a fast series of notes in an improvisation for example, or if you're learning a language and want hear a speaker at a slower speed...\r\n+\r\n+Some time-scaling technique operates in the \"frequency domain\", i.e. using a Fourier transform to get a view of which frequencies are present in the music at any given time. The one I chose in this blog is a frequency domain technique that's both simple, and achieves reasonable quality for most (but definitely not all) audio tracks and is described in the paper \"Improved  Phase  Vocoder\r\n+Time-Scale  Modification  of  Audio\" IEEE  TRANSACTIONS  ON SPEECH  AND  AUDIO  PROCESSING, VOL.  7,  NO.  3,  MAY  1999. See for example: Paper_. If you're interested in the general area of audio time-scaling, the wikipedia entry_ is a very good introduction.\r\n+\r\n+The technique uses a Fast Fourier Transform (FFT), pick peaking to locate pure tones in the audio, and complex rotations to adjust the phase of the Fourier transorm prior to reconstructing the audio signal.\r\n+\r\n+Python/numpy code:\r\n+__________________________________\r\n+The code is shown below. Refer to the paper if you're interested in the \"theoretical\" explanation for what's being done. As you can see, the numpy implementation is simple but not trivial, requiring several steps. Note that the algorithm uses both real and complex numbers: the output of the rfft routine is a complex array, and the rotations are complex numbers. Copy the code below into a time_scaling.py file.\r\n+\r\n+::\r\n+\r\n+    import numpy as np\r\n+\r\n+\r\n+    def hanning(M):\r\n+        if M < 1: return np.array([])\r\n+        if M == 1: return np.ones(1, float)\r\n+        n = np.arange(0, M)\r\n+        return 0.5 - 0.5*np.cos(2.0*np.pi*n/(M-1))\r\n+\r\n+    def computeRotation(X,rotations,fftSize,inputHop,outputHop):\r\n+        # Compute the appropriate roations based on input and output hop.\r\n+        AX = np.abs(X)\r\n+        # Find peaks in AX: values that are larger than their two neighbors on the left and on the right\r\n+        peaks = 2+np.nonzero((AX[2:-2]>AX[1:-3]) & (AX[2:-2]>AX[0:-4]) & (AX[2:-2]>AX[3:-1]) & (AX[2:-2]>AX[4:]))[0]\r\n+        for ii,id in enumerate(peaks):\r\n+            # Compute new rotation.\r\n+            rot = np.exp(2*np.pi*1j*id/fftSize*(outputHop-inputHop))\r\n+            # Cumulate rotation with the one from the previous frame.\r\n+            rotations[id]*=rot\r\n+            # Now spread this rotation to all the neighboring bins (half-way between peaks).\r\n+            if ii==0: left = 0\r\n+            else: left = int(.5*(peaks[ii-1]+id))\r\n+            if ii==len(peaks)-1: right = len(AX)\r\n+            else: right = int(.5*(peaks[ii+1]+id))\r\n+            rotations[left:right]=rotations[id]\r\n+        return rotations\r\n+\r\n+    def process(signal,sRate,factor = 1.):\r\n+        # Process parameters.\r\n+        windowLengthS = 0.060\r\n+        windowHopS = 0.030\r\n+\r\n+        windowLength = int(np.round(windowLengthS*sRate))\r\n+        windowHop = int(np.round(windowHopS*sRate))\r\n+        fftSize = int(2**np.ceil(np.log2(windowLength)))\r\n+        window = np.sqrt(hanning(windowLength))\r\n+        halfWinLen = int(np.floor(windowLength/2))\r\n+\r\n+        curInSamp = 0\r\n+        curOutSamp = 0\r\n+        prevInSamp = curInSamp-windowHop\r\n+        # Initialize rotations. They're complex, and they're the same for all channels.\r\n+        rotations = np.ones(fftSize/2+1,dtype=np.complex)\r\n+        outSig = np.zeros(int(factor*len(signal)),dtype=signal.dtype)\r\n+        xx = np.zeros(fftSize)\r\n+        while 1:\r\n+            if curInSamp+windowLength > len(signal): break\r\n+            if curOutSamp+windowLength > len(outSig): break\r\n+            # Take the fft of the signal starting at curInSamp. It's a good thing to have a zero-phase fft so roll it by\r\n+            # half a window size so the middle of the input window is at t=0\r\n+            xx[0:windowLength] = signal[curInSamp:curInSamp+windowLength] * window\r\n+            xx[windowLength:] = 0\r\n+            xx = np.roll(xx,-halfWinLen)\r\n+            X = np.fft.rfft(xx,fftSize)\r\n+            # Compute required rotations based on the input and output hop.\r\n+            computeRotation(X,rotations,fftSize,curInSamp-prevInSamp,windowHop)\r\n+            # Apply to FFT\r\n+            Y = X * rotations\r\n+            # Take the inverse FFT, undo the circular roll and overlap add into the output signal.\r\n+            yy = np.fft.irfft(Y,fftSize)\r\n+            yy = np.roll(yy,halfWinLen)\r\n+            outSig[curOutSamp:curOutSamp+windowLength] += yy[0:windowLength] * window\r\n+            # Increment the output sample by half a window size, and the input sample according to the time scaling factor.\r\n+            prevInSamp = curInSamp\r\n+            curOutSamp += windowHop\r\n+            curInSamp = int(np.round(curOutSamp/factor))\r\n+\r\n+        return outSig\r\n+\r\n+Now we can run the process function on an audio file. For simplicity I'm using a .wav file: scipy has a very simple interface for reading or writing a .wav file.\r\n+Note that our process function expects a 1D input array. If you open a stereo .wav file, the array returned by wavfile.read will be 2D. In case this happens I'm only keeping the left channel. You can copy paste the following code into a main.py function:\r\n+\r\n+::\r\n+\r\n+    import time_scaling\r\n+    import numpy as np\r\n+    from scipy.io import wavfile\r\n+\r\n+    sRate, data = wavfile.read(r'/Users/jlaroche/temp/MessageInABottleMono.wav')\r\n+    x=data[:,0] if data.ndim == 2 else data\r\n+    factor = 1.2\r\n+    out = time_scaling.process(x.astype(float)/32767,float(sRate),factor)\r\n+    wavfile.write('./out.wav',sRate,(32767*out).astype(np.int16))\r\n+\r\n+You should be able to open the output file and listen to it in any program that plays wavfiles (for example afplay on macos).\r\n+\r\n+Let's time the function in ipython. For this you start ipython (install it if you don't have it, it's a great complement to python).\r\n+In ipython, you can simply put %timeit in front of the line you'd like to benchmark:\r\n+\r\n+::\r\n+\r\n+    %timeit out = time_scaling.process(x.astype(float)/32767,float(sRate),factor)\r\n+\r\n+For the (quite long) wav file I was using, %timeit returned\r\n+::\r\n+\r\n+    1 loop, best of 3: 15.1 s per loop\r\n+\r\n+\r\n+Using pythran:\r\n+__________________________________\r\n+\r\n+\r\n+To be able to use the process function from the module that pythran will create, we need to export it to python. This is what the following #pythran export directive does. This can be placed anywhere in the .py file.\r\n+\r\n+::\r\n+\r\n+    #pythran export process(float[] or float[::],float,float)\r\n+\r\n+Note that the first parameter is declared as float[] or float[::] a simple float numpy array or a view into a float numpy array. The two remaining parameters are declared as float and it will be crucial to pass them as floats when calling process.\r\n+\r\n+Now simply run\r\n+\r\n+::\r\n+\r\n+    pythran time_scaling.py.\r\n+\r\n+A time_scaling.so file is created.\r\n+Now the same main.py code will execute much faster because import time_scaling will now import a compiled, very efficient .so file.\r\n+\r\n+For the same file as above, %timeit now returns:\r\n+::\r\n+\r\n+    1 loop, best of 3: 1.87 s per loop\r\n+\r\n+The speed up is amazing. The function runs about 14 times faster than it did in pure python/numpy.\r\n+Note that if you pass an int instead of a float to the process function time_scaling.process(x.astype(float)/32767,int(sRate),factor) you will get a run-time error so make sure you're passing the very same types you've declared in time_scaling.py.\r\n+\r\n+\r\n+Calling from C++\r\n+__________________________________\r\n+\r\n+As I explained above, one of the most amazing aspects of pythran is that it generates self contained C++ code that can be called from any other C++ program. As I explained, the code is self contained in that it does not require any dlls, and makes no call to the python library. In short, it's a very efficient C++ version of your python/numpy algorithm, fully portable to any target architecture.\r\n+To create a c++ version of our process function, we simply do:\r\n+\r\n+::\r\n+\r\n+    pythran -e time_scaling.py\r\n+\r\n+This creates a file time_scaling.cpp that can then be compiled along with the calling code. Note that in this case, the #pythran export declaration is no longer needed. You can take a look at the C++ code, but it will be extremely cryptic and heavily templated... But that's not a problem as this code never need to be hand-tweaked.\r\n+\r\n+Now, how do we call this process() function from our main C++ program?\r\n+For this, we must pass the audio in a numpy like array, but the pythran C++ source code provides convenient functions to do just that.\r\n+This is the main.cpp file:\r\n+\r\n+::\r\n+\r\n+    #include <stdio.h>      /* printf, scanf, NULL */\r\n+    #include <stdlib.h>\r\n+\r\n+    #include \"numpy/_numpyconfig.h\"\r\n+    #include \"time_scaling.cpp\"\r\n+    #include <pythonic/include/numpy/array.hpp>\r\n+    #include <pythonic/numpy/array.hpp>\r\n+    #include \"pythonic/include/utils/array_helper.hpp\"\r\n+    #include \"pythonic/include/types/ndarray.hpp\"\r\n+\r\n+    using namespace pythonic;\r\n+\r\n+    // Helper to create a float 1D array from a pointer\r\n+    template <typename T>\r\n+    types::ndarray<T, types::pshape<long>> arrayFromBuf1D(T* fPtr, long size) {\r\n+        auto shape = types::pshape<long>(size);\r\n+        return types::ndarray<T, types::pshape<long>>(fPtr,shape,types::ownership::external);\r\n+    }\r\n+\r\n+    #define MAX_NUM_SAMPS 500*44100\r\n+    int main()\r\n+    {\r\n+        // Read audio\r\n+        char* fileName = \"./police.raw\";\r\n+        long L = MAX_NUM_SAMPS;\r\n+        float* ptr = (float*) malloc(L*sizeof(float));\r","path":"prototype-to-fast-c++.rst","position":210,"original_position":210,"commit_id":"4517418583bf6fb394ce21e8cc5914fd0fa0ce0e","original_commit_id":"4517418583bf6fb394ce21e8cc5914fd0fa0ce0e","user":{"login":"serge-sans-paille","id":863807,"node_id":"MDQ6VXNlcjg2MzgwNw==","avatar_url":"https://avatars1.githubusercontent.com/u/863807?v=4","gravatar_id":"","url":"https://api.github.com/users/serge-sans-paille","html_url":"https://github.com/serge-sans-paille","followers_url":"https://api.github.com/users/serge-sans-paille/followers","following_url":"https://api.github.com/users/serge-sans-paille/following{/other_user}","gists_url":"https://api.github.com/users/serge-sans-paille/gists{/gist_id}","starred_url":"https://api.github.com/users/serge-sans-paille/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serge-sans-paille/subscriptions","organizations_url":"https://api.github.com/users/serge-sans-paille/orgs","repos_url":"https://api.github.com/users/serge-sans-paille/repos","events_url":"https://api.github.com/users/serge-sans-paille/events{/privacy}","received_events_url":"https://api.github.com/users/serge-sans-paille/received_events","type":"User","site_admin":false},"body":"or even better : a ``std::unique_ptr``","created_at":"2018-11-27T07:44:23Z","updated_at":"2018-11-27T07:47:16Z","html_url":"https://github.com/serge-sans-paille/pythran-stories/pull/16#discussion_r236548691","pull_request_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/comments/236548691"},"html":{"href":"https://github.com/serge-sans-paille/pythran-stories/pull/16#discussion_r236548691"},"pull_request":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16"}},"in_reply_to_id":236548587},"pull_request":{"url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16","id":233742346,"node_id":"MDExOlB1bGxSZXF1ZXN0MjMzNzQyMzQ2","html_url":"https://github.com/serge-sans-paille/pythran-stories/pull/16","diff_url":"https://github.com/serge-sans-paille/pythran-stories/pull/16.diff","patch_url":"https://github.com/serge-sans-paille/pythran-stories/pull/16.patch","issue_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/16","number":16,"state":"open","locked":false,"title":"First version of the text","user":{"login":"jeanlaroche","id":27791933,"node_id":"MDQ6VXNlcjI3NzkxOTMz","avatar_url":"https://avatars1.githubusercontent.com/u/27791933?v=4","gravatar_id":"","url":"https://api.github.com/users/jeanlaroche","html_url":"https://github.com/jeanlaroche","followers_url":"https://api.github.com/users/jeanlaroche/followers","following_url":"https://api.github.com/users/jeanlaroche/following{/other_user}","gists_url":"https://api.github.com/users/jeanlaroche/gists{/gist_id}","starred_url":"https://api.github.com/users/jeanlaroche/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeanlaroche/subscriptions","organizations_url":"https://api.github.com/users/jeanlaroche/orgs","repos_url":"https://api.github.com/users/jeanlaroche/repos","events_url":"https://api.github.com/users/jeanlaroche/events{/privacy}","received_events_url":"https://api.github.com/users/jeanlaroche/received_events","type":"User","site_admin":false},"body":"Checking it as .rst. Don't like the default formatting when converted to html, but pelican may have a different way.\r\nThere are a couple places where I have questions for you.\r\nAlso if you could review the main.cpp code to ensure I'm not doing anything silly or useless that would be great!\r\n","created_at":"2018-11-27T00:35:39Z","updated_at":"2018-11-27T07:47:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"032802f5cd8dcfd35341d7ccc0dfeebc4bfc934c","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16/commits","review_comments_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16/comments","review_comment_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/comments{/number}","comments_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/16/comments","statuses_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/statuses/4517418583bf6fb394ce21e8cc5914fd0fa0ce0e","head":{"label":"jeanlaroche:gh-pages","ref":"gh-pages","sha":"4517418583bf6fb394ce21e8cc5914fd0fa0ce0e","user":{"login":"jeanlaroche","id":27791933,"node_id":"MDQ6VXNlcjI3NzkxOTMz","avatar_url":"https://avatars1.githubusercontent.com/u/27791933?v=4","gravatar_id":"","url":"https://api.github.com/users/jeanlaroche","html_url":"https://github.com/jeanlaroche","followers_url":"https://api.github.com/users/jeanlaroche/followers","following_url":"https://api.github.com/users/jeanlaroche/following{/other_user}","gists_url":"https://api.github.com/users/jeanlaroche/gists{/gist_id}","starred_url":"https://api.github.com/users/jeanlaroche/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeanlaroche/subscriptions","organizations_url":"https://api.github.com/users/jeanlaroche/orgs","repos_url":"https://api.github.com/users/jeanlaroche/repos","events_url":"https://api.github.com/users/jeanlaroche/events{/privacy}","received_events_url":"https://api.github.com/users/jeanlaroche/received_events","type":"User","site_admin":false},"repo":{"id":159235015,"node_id":"MDEwOlJlcG9zaXRvcnkxNTkyMzUwMTU=","name":"pythran-stories","full_name":"jeanlaroche/pythran-stories","private":false,"owner":{"login":"jeanlaroche","id":27791933,"node_id":"MDQ6VXNlcjI3NzkxOTMz","avatar_url":"https://avatars1.githubusercontent.com/u/27791933?v=4","gravatar_id":"","url":"https://api.github.com/users/jeanlaroche","html_url":"https://github.com/jeanlaroche","followers_url":"https://api.github.com/users/jeanlaroche/followers","following_url":"https://api.github.com/users/jeanlaroche/following{/other_user}","gists_url":"https://api.github.com/users/jeanlaroche/gists{/gist_id}","starred_url":"https://api.github.com/users/jeanlaroche/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jeanlaroche/subscriptions","organizations_url":"https://api.github.com/users/jeanlaroche/orgs","repos_url":"https://api.github.com/users/jeanlaroche/repos","events_url":"https://api.github.com/users/jeanlaroche/events{/privacy}","received_events_url":"https://api.github.com/users/jeanlaroche/received_events","type":"User","site_admin":false},"html_url":"https://github.com/jeanlaroche/pythran-stories","description":"pythran blog","fork":true,"url":"https://api.github.com/repos/jeanlaroche/pythran-stories","forks_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/forks","keys_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/keys{/key_id}","collaborators_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/teams","hooks_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/hooks","issue_events_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/issues/events{/number}","events_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/events","assignees_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/assignees{/user}","branches_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/branches{/branch}","tags_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/tags","blobs_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/git/refs{/sha}","trees_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/git/trees{/sha}","statuses_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/statuses/{sha}","languages_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/languages","stargazers_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/stargazers","contributors_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/contributors","subscribers_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/subscribers","subscription_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/subscription","commits_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/commits{/sha}","git_commits_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/git/commits{/sha}","comments_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/comments{/number}","issue_comment_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/issues/comments{/number}","contents_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/contents/{+path}","compare_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/compare/{base}...{head}","merges_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/merges","archive_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/downloads","issues_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/issues{/number}","pulls_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/pulls{/number}","milestones_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/milestones{/number}","notifications_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/labels{/name}","releases_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/releases{/id}","deployments_url":"https://api.github.com/repos/jeanlaroche/pythran-stories/deployments","created_at":"2018-11-26T21:26:24Z","updated_at":"2018-11-27T00:32:26Z","pushed_at":"2018-11-27T00:32:25Z","git_url":"git://github.com/jeanlaroche/pythran-stories.git","ssh_url":"git@github.com:jeanlaroche/pythran-stories.git","clone_url":"https://github.com/jeanlaroche/pythran-stories.git","svn_url":"https://github.com/jeanlaroche/pythran-stories","homepage":"http://serge-sans-paille.github.io/pythran-stories/","size":3748,"stargazers_count":0,"watchers_count":0,"language":"HTML","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":null,"forks":0,"open_issues":0,"watchers":0,"default_branch":"gh-pages"}},"base":{"label":"serge-sans-paille:gh-pages","ref":"gh-pages","sha":"c92f9d5af360ab3f150aabee67e9162a89c8455e","user":{"login":"serge-sans-paille","id":863807,"node_id":"MDQ6VXNlcjg2MzgwNw==","avatar_url":"https://avatars1.githubusercontent.com/u/863807?v=4","gravatar_id":"","url":"https://api.github.com/users/serge-sans-paille","html_url":"https://github.com/serge-sans-paille","followers_url":"https://api.github.com/users/serge-sans-paille/followers","following_url":"https://api.github.com/users/serge-sans-paille/following{/other_user}","gists_url":"https://api.github.com/users/serge-sans-paille/gists{/gist_id}","starred_url":"https://api.github.com/users/serge-sans-paille/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serge-sans-paille/subscriptions","organizations_url":"https://api.github.com/users/serge-sans-paille/orgs","repos_url":"https://api.github.com/users/serge-sans-paille/repos","events_url":"https://api.github.com/users/serge-sans-paille/events{/privacy}","received_events_url":"https://api.github.com/users/serge-sans-paille/received_events","type":"User","site_admin":false},"repo":{"id":53442021,"node_id":"MDEwOlJlcG9zaXRvcnk1MzQ0MjAyMQ==","name":"pythran-stories","full_name":"serge-sans-paille/pythran-stories","private":false,"owner":{"login":"serge-sans-paille","id":863807,"node_id":"MDQ6VXNlcjg2MzgwNw==","avatar_url":"https://avatars1.githubusercontent.com/u/863807?v=4","gravatar_id":"","url":"https://api.github.com/users/serge-sans-paille","html_url":"https://github.com/serge-sans-paille","followers_url":"https://api.github.com/users/serge-sans-paille/followers","following_url":"https://api.github.com/users/serge-sans-paille/following{/other_user}","gists_url":"https://api.github.com/users/serge-sans-paille/gists{/gist_id}","starred_url":"https://api.github.com/users/serge-sans-paille/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serge-sans-paille/subscriptions","organizations_url":"https://api.github.com/users/serge-sans-paille/orgs","repos_url":"https://api.github.com/users/serge-sans-paille/repos","events_url":"https://api.github.com/users/serge-sans-paille/events{/privacy}","received_events_url":"https://api.github.com/users/serge-sans-paille/received_events","type":"User","site_admin":false},"html_url":"https://github.com/serge-sans-paille/pythran-stories","description":"pythran blog","fork":false,"url":"https://api.github.com/repos/serge-sans-paille/pythran-stories","forks_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/forks","keys_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/keys{/key_id}","collaborators_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/teams","hooks_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/hooks","issue_events_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/events{/number}","events_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/events","assignees_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/assignees{/user}","branches_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/branches{/branch}","tags_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/tags","blobs_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/git/refs{/sha}","trees_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/git/trees{/sha}","statuses_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/statuses/{sha}","languages_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/languages","stargazers_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/stargazers","contributors_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/contributors","subscribers_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/subscribers","subscription_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/subscription","commits_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/commits{/sha}","git_commits_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/git/commits{/sha}","comments_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/comments{/number}","issue_comment_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/comments{/number}","contents_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/contents/{+path}","compare_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/compare/{base}...{head}","merges_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/merges","archive_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/downloads","issues_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues{/number}","pulls_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls{/number}","milestones_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/milestones{/number}","notifications_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/labels{/name}","releases_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/releases{/id}","deployments_url":"https://api.github.com/repos/serge-sans-paille/pythran-stories/deployments","created_at":"2016-03-08T20:14:31Z","updated_at":"2018-11-25T20:07:48Z","pushed_at":"2018-11-27T00:35:40Z","git_url":"git://github.com/serge-sans-paille/pythran-stories.git","ssh_url":"git@github.com:serge-sans-paille/pythran-stories.git","clone_url":"https://github.com/serge-sans-paille/pythran-stories.git","svn_url":"https://github.com/serge-sans-paille/pythran-stories","homepage":"http://serge-sans-paille.github.io/pythran-stories/","size":3741,"stargazers_count":1,"watchers_count":1,"language":"HTML","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":true,"forks_count":4,"mirror_url":null,"archived":false,"open_issues_count":1,"license":null,"forks":4,"open_issues":1,"watchers":1,"default_branch":"gh-pages"}},"_links":{"self":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16"},"html":{"href":"https://github.com/serge-sans-paille/pythran-stories/pull/16"},"issue":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/16"},"comments":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/issues/16/comments"},"review_comments":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16/comments"},"review_comment":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/pulls/16/commits"},"statuses":{"href":"https://api.github.com/repos/serge-sans-paille/pythran-stories/statuses/4517418583bf6fb394ce21e8cc5914fd0fa0ce0e"}},"author_association":"NONE"}}
{"actor":{"display_login":"serge-sans-paille"}}