tag:blogger.com,1999:blog-67210712640634777162024-03-13T10:32:07.861+08:00Emptiness Bloggingfrom emptiness to emptiness ...mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.comBlogger290125tag:blogger.com,1999:blog-6721071264063477716.post-76321601272344467802023-03-24T12:15:00.001+08:002023-03-24T12:15:36.121+08:00一些好數字08, 一個年份、一個月份、一個日子、一個時間<br /><br />一百年前的奧運三問.. 在一百年後, 2008年8月8日晚上8時... 有了答案<br /><br />51、21、28 ... 加起上來, 就是一百!<br /><br />100 , 是一個圓滿的數字.<br /><br /><br /><object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/Z6EZkiYCvWs&hl=en&fs=1"><param name="allowFullScreen" value="true"><embed src="http://www.youtube.com/v/Z6EZkiYCvWs&hl=en&fs=1" type="application/x-shockwave-flash" allowfullscreen="true" width="425" height="344"></embed></object>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-14193836853408117422016-08-06T21:52:00.001+08:002016-08-06T22:08:18.064+08:00Design first<p dir="ltr">After joining the new company, the oracle always mentions "design first!".</p>
<p dir="ltr">At first, I do not really understand why proposing an implementation would get that message in return. Today, I read that again in a slack conversation. As an observer, my brain get flashed with a saying - "Design is problem setting, Planning is problem solving". Then, when that linked to the use cases that oracle keeps asking, I got something.</p>
<p dir="ltr">I'm really lucky to join this company, even though I'm not having much interaction<u>s</u> with the oracle.</p>
mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-56326384824212314432016-03-03T02:35:00.003+08:002016-03-23T23:46:51.114+08:00Things I learnt in the past 5 years<div class="markdown-here-wrapper" data-md-url="https://www.blogger.com/blogger.g?blogID=6721071264063477716#editor/target=post;postID=5632638482421231443;onPublishedMenu=allposts;onClosedMenu=allposts;postNum=0;src=link">
<blockquote style="border-left: 4px solid rgb(221, 221, 221); color: #777777; margin: 1.2em 0px; padding: 0px 1em; quotes: none;">
<div style="margin: 0px 0px 1.2em ! important;">
“It’s hard to teach a new dog old tricks.” - On many annual letters to shareholders written by the Oracle of Omaha</div>
</blockquote>
<div style="margin: 0px 0px 1.2em ! important;">
In the past 5 years, I worked with a team that have 2 product lines being alive while 5 others were dead. It is my pleasure to work with the team that let me learn and strengthen certain tricks. Old tricks take the past to learn and easy to forget, this post reminds my future self.</div>
<h2 id="on-risk-taking" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On risk taking</h2>
<blockquote style="border-left: 4px solid rgb(221, 221, 221); color: #777777; margin: 1.2em 0px; padding: 0px 1em; quotes: none;">
<div style="margin: 0px 0px 1.2em ! important;">
“Over the years, a number of very smart people have learned the hard way that a long stream of impressive numbers multiplied by a single zero always equals zero.” - Buffett</div>
</blockquote>
<div style="margin: 0px 0px 1.2em ! important;">
Startup could try to solve a problem with really new way of doings, only if the environment allows it. In the Valley, the legal and finance system allows a startup to take risky bet until the company gets enough eyeballs and/or moneys. This is not applicable to HK. We don’t have a court that knows technology, law that creates (or, at least try to create) a level playing field, money that supports risky bet, etc. So, being conservative sounds really legitimate here. No matter how much success you have achieved in the past, a single misstep could put you to the end. This also applies to engineering where new technology may not work as advertised, sometimes. Traditional technology with an active ecosystem implies battle scars on the face of others.</div>
<h2 id="on-decision-making" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On decision making</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Data driven is great, only when you can differentiate signal verses noise. Sometimes, a B2B SaaS could use data to drive decision, when the data looks consistent. Often, the sample size is too small to even make a reasonable guess. Leaders give strong opinion but weakly held. When we don’t have the data, we better admit that that is just a guess. (By the way, the infrastructure behinds data collection and data analysis is really hard to get right. I did that 3 times but not even come close to any.)</div>
<h2 id="on-product-design" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On product design</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Minimum Viable Product means minimum, viable, and throw away. Even before you come up with the right question for the audience, how could you come up with an answer? This translates to “doing the right things is more important than doing things right”. Make tests to validate the question. MVP also applies to engineering. Leaky abstraction is bad but not even starting one sucks, whenever you find yourself repeat more than twice. That abstraction leads you to find your ultimate question. And, pre-mature optimization just wastes your time. Let it runs until it hits the wall.</div>
<h2 id="on-culture" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On culture</h2>
<blockquote style="border-left: 4px solid rgb(221, 221, 221); color: #777777; margin: 1.2em 0px; padding: 0px 1em; quotes: none;">
<div style="margin: 0px 0px 1.2em ! important;">
“We shape our buildings and afterwards our buildings shape us.” - Churchill</div>
</blockquote>
<div style="margin: 0px 0px 1.2em ! important;">
Values should be deep-rooted while culture would be ever changing. Culture evolves around the values set by the team. Don’t be afraid to start with something small or vague. Once set, stick with it and shape it. Remember, making something up doesn’t mean doing it. Sometimes, peoples do make mistakes and toxic spreads in the speed of light, delay no more to apply the fix. Eventually, the team would be shaped by it. Engineering do also needs culture and value to scale. We shape our codebase and afterwards our codebase shape us. We eager to find something that can be repeated, nicely.</div>
<h2 id="on-multi-tasking" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On multi-tasking</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Some tasks can be run in parallel and some cannot. Most of the time, the best way is not to multi-task. Take pause, schedule the tasks using whatever you are comfortable with. Delegation enables true multi-tasking. In engineering, data processing shares this as well. Multi-processing sometimes does not work as expected.</div>
<h2 id="on-ownership" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On ownership</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Ownership implies dedication. No-one can own everything since bandwidth is limited. In short term, sole ownership provides cost efficiency. In long term, that is a trap! Knowledge transfer is a daily routine that cannot be prevented. Being an owner of one thing makes you feel doing good, being an owner of multiple things makes you feel doing nothing. How could we take care of many things with the same level of dedication as one? Ownership transfer re-gains dedication.</div>
<h2 id="on-communication" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On communication</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Communication is king. Both written and verbal. Verbal is fast but lossy, written is persistent. They are not mutually exclusive and complement each other. In engineering, written is favored. Commit logs, documentations, source comments, discussion on issues. These help scaling and understanding. Not improving or ignoring it is fool.</div>
<h2 id="on-tooling" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On tooling</h2>
<blockquote style="border-left: 4px solid rgb(221, 221, 221); color: #777777; margin: 1.2em 0px; padding: 0px 1em; quotes: none;">
<div style="margin: 0px 0px 1.2em ! important;">
“Less is more.”</div>
</blockquote>
<div style="margin: 0px 0px 1.2em ! important;">
Having fewer things to care give you focus. Managed service like Heroku is always a good place to start with. Don’t try to build a custom PaaS when that is not your business. Though, the eager to reinvent a wheel should be respected, that gives you better understanding. Your business should already take your 80, the other 20 is better not to get more troubles.</div>
<h2 id="on-meeting" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On meeting</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Meeting has many types and they all need an agenda. It is better used for discussion but brain-storming that could be done alone. Stick to the agenda, come to action items fast. And, treat synchronous meeting as a limited resource.</div>
<h2 id="on-tradeoff" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
On tradeoff</h2>
<blockquote style="border-left: 4px solid rgb(221, 221, 221); color: #777777; margin: 1.2em 0px; padding: 0px 1em; quotes: none;">
<div style="margin: 0px 0px 1.2em ! important;">
“There are many ways to Rome.”</div>
</blockquote>
<div style="margin: 0px 0px 1.2em ! important;">
Most of the decisions have opportunity cost, taking one path may lose upsides of another. What really matter is whether that achieves the goal. And, making the tradeoff verbose provides better understanding. That could also gain support from the team.</div>
<h2 id="last-but-not-least" style="border-bottom: 1px solid rgb(238, 238, 238); font-size: 1.4em; font-weight: bold; margin: 1.3em 0px 1em; padding: 0px;">
Last but not least</h2>
<div style="margin: 0px 0px 1.2em ! important;">
Stay hungry. Stay foolish. Self-explanatory.</div>
<div style="margin: 0px 0px 1.2em ! important;">
Thanks <a href="https://twitter.com/anthonycyl">@anthonycyl</a> for the edit.</div>
<div style="font-size: 0em; height: 0; margin: 0; max-height: 0; max-width: 0; overflow: hidden; padding: 0; width: 0;" title="MDH:PHA+CjxzcGFuPjxzcGFuPiZndDsgIkl0J3MgaGFyZCB0byB0ZWFjaCBhIG5ldyBkb2cgb2xkIHRy
aWNrcy4iIC0gT24gbWFueSBhbm51YWwgbGV0dGVycyB0byBzaGFyZWhvbGRlcnMgd3JpdHRlbiBi
eSB0aGUgT3JhY2xlIG9mIE9tYWhhPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48
c3Bhbj5JbiB0aGUgcGFzdCA1IHllYXJzLCBJIHdvcmtlZCB3aXRoIGEgdGVhbSB0aGF0IGhhdmUg
MiBwcm9kdWN0IGxpbmVzIGJlaW5nIGFsaXZlIHdoaWxlIDUgb3RoZXJzIHdlcmUgZGVhZC4gSXQg
aXMgbXkgcGxlYXN1cmUgdG8gd29yayB3aXRoIHRoZSB0ZWFtIHRoYXQgbGV0IG1lIGxlYXJuIGFu
ZCBzdHJlbmd0aGVuIGNlcnRhaW4gdHJpY2tzLiBPbGQgdHJpY2tzIHRha2UgdGhlIHBhc3QgdG8g
bGVhcm4gYW5kIGVhc3kgdG8gZm9yZ2V0LCB0aGlzIHBvc3QgcmVtaW5kcyBteSBmdXR1cmUgc2Vs
Zi48L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxkaXY+PHNwYW4+PGJyPjwvc3Bh
bj48L2Rpdj48c3Bhbj5PbiByaXNrIHRha2luZzxicj4KLS0tLS0tLS0tLS0tLS0tLS08L3NwYW4+
PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPiZndDsgIk92ZXIgdGhlIHllYXJzLCBh
IG51bWJlciBvZiB2ZXJ5IHNtYXJ0IHBlb3BsZSBoYXZlIGxlYXJuZWQgdGhlIGhhcmQgd2F5IHRo
YXQgYSBsb25nIHN0cmVhbSBvZiBpbXByZXNzaXZlIG51bWJlcnMgbXVsdGlwbGllZCBieSBhIHNp
bmdsZSB6ZXJvIGFsd2F5cyBlcXVhbHMgemVyby4iIC0gQnVmZmV0dDwvc3Bhbj48ZGl2PjxzcGFu
Pjxicj48L3NwYW4+PC9kaXY+PHNwYW4+U3RhcnR1cCBjb3VsZCB0cnkgdG8gc29sdmUgYSBwcm9i
bGVtIHdpdGggcmVhbGx5IG5ldyB3YXkgb2YgZG9pbmdzLCBvbmx5IGlmIHRoZSBlbnZpcm9ubWVu
dCBhbGxvd3MgaXQuIEluIHRoZSBWYWxsZXksIHRoZSBsZWdhbCBhbmQgZmluYW5jZSBzeXN0ZW0g
YWxsb3dzIGEgc3RhcnR1cCB0byB0YWtlIHJpc2t5IGJldCB1bnRpbCB0aGUgY29tcGFueSBnZXRz
IGVub3VnaCBleWViYWxscyBhbmQvb3IgbW9uZXlzLiBUaGlzIGlzIG5vdCBhcHBsaWNhYmxlIHRv
IEhLLiBXZSBkb27igJl0IGhhdmUgYSBjb3VydCB0aGF0IGtub3dzIHRlY2hub2xvZ3ksIGxhdyB0
aGF0IGNyZWF0ZXMgKG9yLCBhdCBsZWFzdCB0cnkgdG8gY3JlYXRlKSBhIGxldmVsIHBsYXlpbmcg
ZmllbGQsIG1vbmV5IHRoYXQgc3VwcG9ydHMgcmlza3kgYmV0LCBldGMuIFNvLCBiZWluZyBjb25z
ZXJ2YXRpdmUgc291bmRzIHJlYWxseSBsZWdpdGltYXRlIGhlcmUuIE5vIG1hdHRlciBob3cgbXVj
aCBzdWNjZXNzIHlvdSBoYXZlIGFjaGlldmVkIGluIHRoZSBwYXN0LCBhIHNpbmdsZSBtaXNzdGVw
IGNvdWxkIHB1dCB5b3UgdG8gdGhlIGVuZC4gVGhpcyBhbHNvIGFwcGxpZXMgdG8gZW5naW5lZXJp
bmcgd2hlcmUgbmV3IHRlY2hub2xvZ3kgbWF5IG5vdCB3b3JrIGFzIGFkdmVydGlzZWQsIHNvbWV0
aW1lcy4gVHJhZGl0aW9uYWwgdGVjaG5vbG9neSB3aXRoIGFuIGFjdGl2ZSBlY29zeXN0ZW0gaW1w
bGllcyBiYXR0bGUgc2NhcnMgb24gdGhlIGZhY2Ugb2Ygb3RoZXJzLjwvc3Bhbj48ZGl2PjxzcGFu
Pjxicj48L3NwYW4+PC9kaXY+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPk9uIGRl
Y2lzaW9uIG1ha2luZzxicj4KLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tPC9zcGFuPjxkaXY+PHNw
YW4+PGJyPjwvc3Bhbj48L2Rpdj48c3Bhbj5EYXRhIGRyaXZlbiBpcyBncmVhdCwgb25seSB3aGVu
IHlvdSBjYW4gZGlmZmVyZW50aWF0ZSBzaWduYWwgdmVyc2VzIG5vaXNlLiBTb21ldGltZXMsIGEg
QjJCIFNhYVMgY291bGQgdXNlIGRhdGEgdG8gZHJpdmUgZGVjaXNpb24sIHdoZW4gdGhlIGRhdGEg
bG9va3MgY29uc2lzdGVudC4gT2Z0ZW4sIHRoZSBzYW1wbGUgc2l6ZSBpcyB0b28gc21hbGwgdG8g
ZXZlbiBtYWtlIGEgcmVhc29uYWJsZSBndWVzcy4gTGVhZGVycyBnaXZlIHN0cm9uZyBvcGluaW9u
IGJ1dCB3ZWFrbHkgaGVsZC4gV2hlbiB3ZSBkb27igJl0IGhhdmUgdGhlIGRhdGEsIHdlIGJldHRl
ciBhZG1pdCB0aGF0IHRoYXQgaXMganVzdCBhIGd1ZXNzLiAoQnkgdGhlIHdheSwgdGhlIGluZnJh
c3RydWN0dXJlIGJlaGluZHMgZGF0YSBjb2xsZWN0aW9uIGFuZCBkYXRhIGFuYWx5c2lzIGlzIHJl
YWxseSBoYXJkIHRvIGdldCByaWdodC4gSSBkaWQgdGhhdCAzIHRpbWVzIGJ1dCBub3QgZXZlbiBj
b21lIGNsb3NlIHRvIGFueS4pPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48ZGl2
PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+T24gcHJvZHVjdCBkZXNpZ248YnI+Ci0tLS0t
LS0tLS0tLS0tLS0tLS0tLS08L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFu
Pk1pbmltdW0gVmlhYmxlIFByb2R1Y3QgbWVhbnMgbWluaW11bSwgdmlhYmxlLCBhbmQgdGhyb3cg
YXdheS4gRXZlbiBiZWZvcmUgeW91IGNvbWUgdXAgd2l0aCB0aGUgcmlnaHQgcXVlc3Rpb24gZm9y
IHRoZSBhdWRpZW5jZSwgaG93IGNvdWxkIHlvdSBjb21lIHVwIHdpdGggYW4gYW5zd2VyPyBUaGlz
IHRyYW5zbGF0ZXMgdG8gImRvaW5nIHRoZSByaWdodCB0aGluZ3MgaXMgbW9yZSBpbXBvcnRhbnQg
dGhhbiBkb2luZyB0aGluZ3MgcmlnaHQiLiBNYWtlIHRlc3RzIHRvIHZhbGlkYXRlIHRoZSBxdWVz
dGlvbi4gTVZQIGFsc28gYXBwbGllcyB0byBlbmdpbmVlcmluZy4gTGVha3kgYWJzdHJhY3Rpb24g
aXMgYmFkIGJ1dCBub3QgZXZlbiBzdGFydGluZyBvbmUgc3Vja3MsIHdoZW5ldmVyIHlvdSBmaW5k
IHlvdXJzZWxmIHJlcGVhdCBtb3JlIHRoYW4gdHdpY2UuIFRoYXQgYWJzdHJhY3Rpb24gbGVhZHMg
eW91IHRvIGZpbmQgeW91ciB1bHRpbWF0ZSBxdWVzdGlvbi4gQW5kLCBwcmUtbWF0dXJlIG9wdGlt
aXphdGlvbiBqdXN0IHdhc3RlcyB5b3VyIHRpbWUuIExldCBpdCBydW5zIHVudGlsIGl0IGhpdHMg
dGhlIHdhbGwuPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48ZGl2PjxzcGFuPjxi
cj48L3NwYW4+PC9kaXY+PHNwYW4+T24gY3VsdHVyZTxicj4KLS0tLS0tLS0tLS0tLTwvc3Bhbj48
ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+Jmd0OyAiV2Ugc2hhcGUgb3VyIGJ1aWxk
aW5ncyBhbmQgYWZ0ZXJ3YXJkcyBvdXIgYnVpbGRpbmdzIHNoYXBlIHVzLiIgLSBDaHVyY2hpbGw8
L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPlZhbHVlcyBzaG91bGQgYmUg
ZGVlcC1yb290ZWQgd2hpbGUgY3VsdHVyZSB3b3VsZCBiZSBldmVyIGNoYW5naW5nLiBDdWx0dXJl
IGV2b2x2ZXMgYXJvdW5kIHRoZSB2YWx1ZXMgc2V0IGJ5IHRoZSB0ZWFtLiBEb27igJl0IGJlIGFm
cmFpZCB0byBzdGFydCB3aXRoIHNvbWV0aGluZyBzbWFsbCBvciB2YWd1ZS4gT25jZSBzZXQsIHN0
aWNrIHdpdGggaXQgYW5kIHNoYXBlIGl0LiBSZW1lbWJlciwgbWFraW5nIHNvbWV0aGluZyB1cCBk
b2VzbuKAmXQgbWVhbiBkb2luZyBpdC4gU29tZXRpbWVzLCBwZW9wbGVzIGRvIG1ha2UgbWlzdGFr
ZXMgYW5kIHRveGljIHNwcmVhZHMgaW4gdGhlIHNwZWVkIG9mIGxpZ2h0LCBkZWxheSBubyBtb3Jl
IHRvIGFwcGx5IHRoZSBmaXguIEV2ZW50dWFsbHksIHRoZSB0ZWFtIHdvdWxkIGJlIHNoYXBlZCBi
eSBpdC4gRW5naW5lZXJpbmcgZG8gYWxzbyBuZWVkcyBjdWx0dXJlIGFuZCB2YWx1ZSB0byBzY2Fs
ZS4gV2Ugc2hhcGUgb3VyIGNvZGViYXNlIGFuZCBhZnRlcndhcmRzIG91ciBjb2RlYmFzZSBzaGFw
ZSB1cy4gV2UgZWFnZXIgdG8gZmluZCBzb21ldGhpbmcgdGhhdCBjYW4gYmUgcmVwZWF0ZWQsIG5p
Y2VseS48L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxkaXY+PHNwYW4+PGJyPjwv
c3Bhbj48L2Rpdj48c3Bhbj5PbiBtdWx0aS10YXNraW5nPGJyPgotLS0tLS0tLS0tLS0tLS0tLS0t
LTwvc3Bhbj48ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+U29tZSB0YXNrcyBjYW4g
YmUgcnVuIGluIHBhcmFsbGVsIGFuZCBzb21lIGNhbm5vdC4gTW9zdCBvZiB0aGUgdGltZSwgdGhl
IGJlc3Qgd2F5IGlzIG5vdCB0byBtdWx0aS10YXNrLiBUYWtlIHBhdXNlLCBzY2hlZHVsZSB0aGUg
dGFza3MgdXNpbmcgd2hhdGV2ZXIgeW91IGFyZSBjb21mb3J0YWJsZSB3aXRoLiBEZWxlZ2F0aW9u
IGVuYWJsZXMgdHJ1ZSBtdWx0aS10YXNraW5nLiBJbiBlbmdpbmVlcmluZywgZGF0YSBwcm9jZXNz
aW5nIHNoYXJlcyB0aGlzIGFzIHdlbGwuIE11bHRpLXByb2Nlc3Npbmcgc29tZXRpbWVzIGRvZXMg
bm90IHdvcmsgYXMgZXhwZWN0ZWQuPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48
ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+T24gb3duZXJzaGlwPGJyPgotLS0tLS0t
LS0tLS0tLS0tLTwvc3Bhbj48ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+T3duZXJz
aGlwIGltcGxpZXMgZGVkaWNhdGlvbi4gTm8tb25lIGNhbiBvd24gZXZlcnl0aGluZyBzaW5jZSBi
YW5kd2lkdGggaXMgbGltaXRlZC4gSW4gc2hvcnQgdGVybSwgc29sZSBvd25lcnNoaXAgcHJvdmlk
ZXMgY29zdCBlZmZpY2llbmN5LiBJbiBsb25nIHRlcm0sIHRoYXQgaXMgYSB0cmFwISBLbm93bGVk
Z2UgdHJhbnNmZXIgaXMgYSBkYWlseSByb3V0aW5lIHRoYXQgY2Fubm90IGJlIHByZXZlbnRlZC4g
QmVpbmcgYW4gb3duZXIgb2Ygb25lIHRoaW5nIG1ha2VzIHlvdSBmZWVsIGRvaW5nIGdvb2QsIGJl
aW5nIGFuIG93bmVyIG9mIG11bHRpcGxlIHRoaW5ncyBtYWtlcyB5b3UgZmVlbCBkb2luZyBub3Ro
aW5nLiBIb3cgY291bGQgd2UgdGFrZSBjYXJlIG9mIG1hbnkgdGhpbmdzIHdpdGggdGhlIHNhbWUg
bGV2ZWwgb2YgZGVkaWNhdGlvbiBhcyBvbmU/IE93bmVyc2hpcCB0cmFuc2ZlciByZS1nYWlucyBk
ZWRpY2F0aW9uLjxicj48L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxkaXY+PHNw
YW4+PGJyPjwvc3Bhbj48L2Rpdj48c3Bhbj5PbiBjb21tdW5pY2F0aW9uPGJyPgotLS0tLS0tLS0t
LS0tLS0tLS0tLS0tLTwvc3Bhbj48ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+PHNwYW4+Q29t
bXVuaWNhdGlvbiBpcyBraW5nLiBCb3RoIHdyaXR0ZW4gYW5kIHZlcmJhbC4gVmVyYmFsIGlzIGZh
c3QgYnV0IGxvc3N5LCB3cml0dGVuIGlzIHBlcnNpc3RlbnQuIFRoZXkgYXJlIG5vdCBtdXR1YWxs
eSBleGNsdXNpdmUgYW5kIGNvbXBsZW1lbnQgZWFjaCBvdGhlci4gSW4gZW5naW5lZXJpbmcsIHdy
aXR0ZW4gaXMgZmF2b3JlZC4gQ29tbWl0IGxvZ3MsIGRvY3VtZW50YXRpb25zLCBzb3VyY2UgY29t
bWVudHMsIGRpc2N1c3Npb24gb24gaXNzdWVzLiBUaGVzZSBoZWxwIHNjYWxpbmcgYW5kIHVuZGVy
c3RhbmRpbmcuIE5vdCBpbXByb3Zpbmcgb3IgaWdub3JpbmcgaXQgaXMgZm9vbC48L3NwYW4+PGRp
dj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48c3Bh
bj5PbiB0b29saW5nPGJyPgotLS0tLS0tLS0tLS0tPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bh
bj48L2Rpdj48c3Bhbj4mZ3Q7ICJMZXNzIGlzIG1vcmUuIjwvc3Bhbj48ZGl2PjxzcGFuPjxicj48
L3NwYW4+PC9kaXY+PHNwYW4+SGF2aW5nIGZld2VyIHRoaW5ncyB0byBjYXJlIGdpdmUgeW91IGZv
Y3VzLiBNYW5hZ2VkIHNlcnZpY2UgbGlrZSBIZXJva3UgaXMgYWx3YXlzIGEgZ29vZCBwbGFjZSB0
byBzdGFydCB3aXRoLiBEb27igJl0IHRyeSB0byBidWlsZCBhIGN1c3RvbSBQYWFTIHdoZW4gdGhh
dCBpcyBub3QgeW91ciBidXNpbmVzcy4gVGhvdWdoLCB0aGUgZWFnZXIgdG8gcmVpbnZlbnQgYSB3
aGVlbCBzaG91bGQgYmUgcmVzcGVjdGVkLCB0aGF0IGdpdmVzIHlvdSBiZXR0ZXIgdW5kZXJzdGFu
ZGluZy4gWW91ciBidXNpbmVzcyBzaG91bGQgYWxyZWFkeSB0YWtlIHlvdXIgODAsIHRoZSBvdGhl
ciAyMCBpcyBiZXR0ZXIgbm90IHRvIGdldCBtb3JlIHRyb3VibGVzLjwvc3Bhbj48ZGl2PjxzcGFu
Pjxicj48L3NwYW4+PC9kaXY+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPk9uIG1l
ZXRpbmc8YnI+Ci0tLS0tLS0tLS0tLS0tPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rp
dj48c3Bhbj5NZWV0aW5nIGhhcyBtYW55IHR5cGVzIGFuZCB0aGV5IGFsbCBuZWVkIGFuIGFnZW5k
YS4gSXQgaXMgYmV0dGVyIHVzZWQgZm9yIGRpc2N1c3Npb24gYnV0IGJyYWluLXN0b3JtaW5nIHRo
YXQgY291bGQgYmUgZG9uZSBhbG9uZS4gU3RpY2sgdG8gdGhlIGFnZW5kYSwgY29tZSB0byBhY3Rp
b24gaXRlbXMgZmFzdC4gQW5kLCB0cmVhdCBzeW5jaHJvbm91cyBtZWV0aW5nIGFzIGEgbGltaXRl
ZCByZXNvdXJjZS48L3NwYW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxkaXY+PHNwYW4+
PGJyPjwvc3Bhbj48L2Rpdj48c3Bhbj5PbiB0cmFkZW9mZjxicj4KLS0tLS0tLS0tLS0tLS08L3Nw
YW4+PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPiZndDsgIlRoZXJlIGFyZSBtYW55
IHdheXMgdG8gUm9tZS4iPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48c3Bhbj5N
b3N0IG9mIHRoZSBkZWNpc2lvbnMgaGF2ZSBvcHBvcnR1bml0eSBjb3N0LCB0YWtpbmcgb25lIHBh
dGggbWF5IGxvc2UgdXBzaWRlcyBvZiBhbm90aGVyLiBXaGF0IHJlYWxseSBtYXR0ZXIgaXMgd2hl
dGhlciB0aGF0IGFjaGlldmVzIHRoZSBnb2FsLiBBbmQsIG1ha2luZyB0aGUgdHJhZGVvZmYgdmVy
Ym9zZSBwcm92aWRlcyBiZXR0ZXIgdW5kZXJzdGFuZGluZy4gVGhhdCBjb3VsZCBhbHNvIGdhaW4g
c3VwcG9ydCBmcm9tIHRoZSB0ZWFtLjwvc3Bhbj48ZGl2PjxzcGFuPjxicj48L3NwYW4+PC9kaXY+
PGRpdj48c3Bhbj48YnI+PC9zcGFuPjwvZGl2PjxzcGFuPkxhc3QgYnV0IG5vdCBsZWFzdDxicj4K
LS0tLS0tLS0tLS0tLS0tLS0tLS0tPC9zcGFuPjxkaXY+PHNwYW4+PGJyPjwvc3Bhbj48L2Rpdj48
c3Bhbj5TdGF5IGh1bmdyeS4gU3RheSBmb29saXNoLiBTZWxmLWV4cGxhbmF0b3J5Ljwvc3Bhbj48
L3NwYW4+PC9wPjxwPjxzcGFuPjxzcGFuPjxicj48L3NwYW4+PC9zcGFuPjwvcD48cD48c3Bhbj48
c3Bhbj48YnI+PC9zcGFuPjwvc3Bhbj48L3A+PHA+PHNwYW4+PHNwYW4+PC9zcGFuPjwvc3Bhbj5U
aGFua3MgW0BhbnRob255Y3lsXShodHRwczovL3R3aXR0ZXIuY29tL2FudGhvbnljeWwpIGZvciB0
aGUgZWRpdC48YnI+PC9wPg==">
</div>
</div>
mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com2tag:blogger.com,1999:blog-6721071264063477716.post-42612496533282099992015-10-02T16:20:00.000+08:002015-10-07T19:31:22.753+08:00Rate limiting Shopify API using Cuttle<div dir="ltr">
During the development of a Shopify app, it is required to respect the API rate limit set by Shopify. Typically, we can use sleep() statement to make pause between API calls. This simple method works great until there are multiple processes that make API calls concurrently.</div>
<br>
<div dir="ltr">
There are quite a number of ways to solve the problem.</div>
<br>
<div dir="ltr">
1. Serialize all API calls into a single process, though not all business logics can work in this way.<br />
2. Host a RPC server / use a task queue to make API calls. The RPC server / queue manager has to rate limit the API calls. [<a href="http://product.reverb.com/2015/03/07/shopify-rate-limits-sidekiq-and-you/">http://product.reverb.com/2015/03/07/shopify-rate-limits-sidekiq-and-you/</a>]<br />
3. Centralize all API calls with a HTTP proxy where the proxy performs rate limiting.</div>
<br>
<div dir="ltr">
Personally, I think the RPC server / task queue option is quite heavy weighted since that requires:</div>
<br>
<div dir="ltr">
* A RPC / task framework, and<br />
* A RPC server / task queue, and<br />
* A rate limit system built around the RPC server / task queue.</div>
<br>
<div dir="ltr">
In contrast, the HTTP proxy option only requires a HTTP proxy server plus a HTTP client. And, HTTP is well supported in many programming languages and systems. It sounds as a great starting point.</div>
<br>
<div dir="ltr">
(BTW, HTTP can be considered as the underlying protocol of a RPC system.)</div>
<br>
<div dir="ltr">
With the HTTP proxy option, there are quite a few options to get started.</div>
<br>
<div dir="ltr">
1. Use Nginx reverse proxy to wrap the API, use its limit module to perform simple rate limit or write a Lua/JS plugin for more sophisticated control. [<a href="http://codetunes.com/2011/outbound-api-rate-limits-the-nginx-way/">http://codetunes.com/2011/outbound-api-rate-limits-the-nginx-way/</a>]<br />
2. Use Squid forward proxy to perform simple rate limit by client info (e.g. IP address).</div>
<br>
<div dir="ltr">
At the first glance, the Nginx reverse proxy option looks superior since we can have sophisticated rate limit control deployed. Though, using such approach would need to use the Nginx wrapped URL of Shopify API. Or, we have to modify DNS/host configuration to route the traffic.</div>
<br>
<div dir="ltr">
Personally, I am not comfortable in modifying the URL to Shopify API since that may prevent a smooth upgrade of the Shopify API client in the future. For the DNS option, shall I modify the DNS config once per a new Shopify store install the app?</div>
<br>
<div dir="ltr">
(We may also route all traffic to the default virtual host of Nginx and use Lua/JS plugin for the host routing. This does not require URL wrapping or DNS configuration. Though, I personally think this is kinda abusing Nginx.)</div>
<br>
<div dir="ltr">
So, reverse proxy may not be a good way to go. Let's come to the forward proxy option. In this case, we do not need to do anything on the URL to Shopify API and just let the traffic goes through the proxy by configuring the HTTP client. A forward proxy with rate limit control sounds like a good way to go.</div>
<br>
<div dir="ltr">
Here, we come to Cuttle proxy. [<a href="http://github.com/mrkschan/cuttle">http://github.com/mrkschan/cuttle</a>]</div>
<br>
<div dir="ltr">
Cuttle proxy is a HTTP forward proxy solely designed for outbound traffic rate limit using goroutine. It would provide a set of rate limit controls for different scenarios. In case of Shopify API, we can use the following Cuttle settings to perform rate limiting.</div>
<br>
<div dir="ltr">
<pre class="gist">
addr: :3128
zones:
- host: "*.myshopify.com"
shared: false
control: rps
rate: 2
- host: "*"
shared: true
control: noop
</pre>
</div>
<br>
<div dir="ltr">
Then, set the HTTP proxy of the Shopify API client like below to route API calls through Cuttle.</div>
<br>
<div dir="ltr">
<pre class="gist">
# apiclient.py
import shopify
shop_url = 'https://{}:{}@{}/admin'.format(API_KEY, PASSWORD, SHOPIFY_DOMAIN)
shopify.ShopifyResource.set_site(shop_url)
print json.dumps(shopify.Shop.current().to_dict())
# Run
HTTPS_PROXY=127.0.0.1:3128 python apiclient.py
</pre>
</div>
<br>
<div dir="ltr">
As long as all API clients are configured to use Cuttle, API calls will be rate limited at 2 requests per second per Shopify store. So, the rate limit bucket would rarely go empty.</div>
<br>
<div dir="ltr">
Note: It is up to you to set the rate of API calls in Cuttle, using 3 requests per second per store would be another great option. You will receive HTTP 429 sent by Shopify roughly after 120 continouos API calls to the same store over 40 seconds.</div>
<br>
<div dir="ltr">
Note: API calls will be forwarded by Cuttle using the first come first serve manner. If the concurrency level of API calls to the same Shopify store is high, some API calls will wait for a significant amount of time instead of receiving HTTP 429 sent by Shopify immediately. Remember to set a reasonable HTTP timeout in that case.</div>
<br>
<div dir="ltr">
(FYI, the Shopify API rate limit only favors high concurrency level for a short duration. If you really need that in your case, Cuttle would not be a good option.)</div>
mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com5tag:blogger.com,1999:blog-6721071264063477716.post-67650590485124641222015-05-15T22:35:00.000+08:002015-05-15T22:35:04.199+08:00Scope finding in a source file<div>
This post is going to discuss an issue I met when building a text
editor plugin that tries to find the class/function scope which the
current line on the editor belongs to
(<a href="http://atom.io/packages/ctags-status">http://atom.io/packages/ctags-status</a>). The problem I met can be broken
into two parts: (i) Given a set of ranges that may be overlapping on a
one dimension plane, find the ranges that cover a point on the plane.
(ii) Given a set of overlapping ranges, get the topmost range where the
height of ranges follows the ascending order of the starting point of
all ranges (the higher in the stack, the later in the sequence). Note,
the issue is not a hard problem. This post documents how I encounter and
work on the problem.<div>
<br /></div>
So, here is the story.<div>
<br /></div>
When
I build the early version of the plugin, I want to ship it as soon as
possible and see if it is downloaded by anyone (Atom editor does not
expose plugin usage data to its author yet, so the only number I have is
downloads). Thus, there was not much thought process in those days.<div>
<br /></div>
The
early implementation models each scope as a range with start and end
line. To find the scope that the current line belongs to, the problem
becomes a range search problem. Ranges would be overlapping when there
is nested scope. In that case, the start and end lines of the inner
scope would always be enclosed by those of the outer scopes. So, I can
sort all scopes by their start line in ascending order, and the innerest
scope on the current line would be the last one in the sequence that
its line range encloses the current line. This is a O(N log N)
preprocessing + O(N) lookup. I was happy with it.<div>
<br /></div>
So far so good?<div>
<br /></div>
The
issue was not surfaced until I used the plugin to browse a long source
file that has dozens of functions (yup, shouldn't the file be split for
readability?). When I kept moving down the cursor for a while, its
movement was no longer smooth. The issue was that the plugin needs to
find the scope upon each cursor line change. When I fired up the
profiler, I found 300 - 400ms were spent on scope finding when there
were dozens of continuous cursor line changes. I was not sure whether
the plugin was really the cause of the UX problem but it is the one that
took most of the processing time. So, time for optimization!<div>
<br /></div>
Since
this is a range search problem, KD tree, segment tree, and interval
tree quickly came to my mind. There are several factors to consider in
picking a solution: (i) availability of existing implementation (I don't
like reinventing without enhancement), (ii) speed of insert / delete /
update (when a source file is edited, there is a high chance that scopes
are moved), and (iii) lookup speed of course. When I was still deciding
which search tree best fits the issue, I raised a questions to myself.
Why don't simply hash the scope(s) on each line? A simple hash with a
stack in each bucket is a good fit because:<div>
<br /></div>
(i) I just need JavaScript object (hash) and array (stack) to build it.<br />(ii)
A typical source file has less than thousand of lines with a dozen of
scopes. The worst case is having thousands of pointers (lines * scopes)
referring to a dozen of strings (scope names). That should not take a
lot of spaces.<br />(iii) A file edit would introduce quite a lot of scope
movements (e.g. insert a new line at top of the file pushes all scopes
down). Maintaining a data structure via insert / update / delete is like
rebuilding it in the worst case. Building a big hash takes O(NL),
number of scopes * number of lines in the file (which is several
thousands of iterations). The hash building process is offline and I
don't expect it would take long, so I am happy with that.<br />(iv) O(1) lookup, the best that I can get.<div>
<br /></div>
As a result, the plugin is using a hash for scope finding.<div>
<br /></div>
</div>
mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-45329525910787069212014-02-23T11:10:00.001+08:002015-10-02T16:25:19.990+08:00Python descriptor, Django CharField with encryptionThis post is part of the <a href="http://mrkschan.blogspot.hk/search/label/pyfun">pyfun</a> series, I will try to *log* some of the features that I think they make Python funny :)<br />
<br />
One of the most recent topics in my reading list is Python descriptor.<br />
<blockquote class="tr_bq">
An object attribute with “binding behavior”, one
whose attribute access has been overridden by methods in the descriptor
protocol. Those methods are <tt class="xref py py-meth docutils literal"><span class="pre">__get__()</span></tt>, <tt class="xref py py-meth docutils literal"><span class="pre">__set__()</span></tt>, and
<tt class="xref py py-meth docutils literal"><span class="pre">__delete__()</span></tt>. If any of those methods are defined for an object, it is
said to be a descriptor. - <a href="http://docs.python.org/2/howto/descriptor.html">http://docs.python.org/2/howto/descriptor.html</a></blockquote>
When I finish the howto on python.org, I don't really understand what is it and thus my read-later list kept expanding with a lot of related articles; until I came across <a href="http://nbviewer.ipython.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb">this post</a> (If you don't know what is Python descriptor, I recommend you to read the post first since I'm not here to re-post the details with my poor English).<br />
<br />
The purpose of this post is to extend the recommended <a href="http://nbviewer.ipython.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb">reading</a> to provide another example use of Python descriptor - a encryption/decryption wrapper of a Django `CharField`.<br />
<br />
One of the major purpose to implement a Python descriptor is to provide getter and setter to attributes. In some traditional programming languages, we have to implement/generate a set of getter and setter to protect the read/write access of attributes. Or, we can use a generic attribute class that has the protection but the access of the attributes looks like `object.attribute.get()` and `object.attribute.set(xxx)`. Python descriptor solves both of the mentioned problems.<br />
<br />
To encrypt/decrypt a `CharField`, it is obvious to override its `get()`/`set()` functions. We can simply do so by extending the `CharField` just like this <a href="https://djangosnippets.org/snippets/1095/">snippets</a>. However, I would like to demonstrate the use of Python descriptor (yep, I'm abusing it here).<br />
<br />
At first, we need the descriptor with encryption and decryption. The cipher we use here is a simple 32-bytes XOR without padding (which is simply uesless in most of the cases).<br />
<br />
<pre class="gist">
class EncryptedAttr(object):
'''Descriptor that encrypt content on write, decrypt on read'''
def __init__(self, attr, secret_key):
self.attr = attr
self.key = secret_key
def encrypt(self, v):
'''A simple XOR chiper'''
return ''.join(chr(ord(a) ^ ord(b)) for (a, b) in zip(self.key, v))
def decrypt(self, v):
'''A simple XOR chiper'''
return ''.join(chr(ord(a) ^ ord(b)) for (a, b) in zip(self.key, v))
def __get__(self, obj, klass):
'''Get `attr` from owner, and decrypt it'''
cipher_text = getattr(obj, self.attr, None)
if not cipher_text:
return ''
return self.decrypt(cipher_text)
def __set__(self, obj, value):
'''Encrypt value, and set to owner via `attr`'''
if not value:
setattr(obj, self.attr, '')
return
cipher_text = self.encrypt(value)
setattr(obj, self.attr, cipher_text)
</pre>
<br />
The descriptor requires a Django model attribute name and a secret key in its constructor. The attribute name is used to look up the wrapped attribute of the Django model in its `__get__()` and `__set__()` functions. To use it, we just assign it as an attribute to the model class.<br />
<br />
<pre class="gist">
class Secret(models.Model):
wrapped = models.CharField(max_length=32)
content = EncryptedAttr('wrapped', 'This is the 32-bytes secret key.')
# Let's make a secret
payload = 'The secret must be 32-bytes long' # Because we use a 32-bytes XOR
s = Secret()
s.content = payload
s.wrapped
>>> '32-bytes blah blah blah blah ...'
s.content
>>> 'The secret must be 32-bytes long'
</pre>
<br />
In this example, the CharField `wrapped` attribute is not expected to be accessed directly. When we assign plain text to `content`, the plain text is encrypted and stored to `wrapped`. The `content` attribute does not hold anything at all. On the other hand, when we read from `content` attribute, it actually decrypts the cipher text from `wrapped`.<br />
<br />
You may get the sample Django project to play around at <a href="https://github.com/mrkschan/encrypted-field">https://github.com/mrkschan/encrypted-field</a>.mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com3tag:blogger.com,1999:blog-6721071264063477716.post-52271854776246314182013-10-17T18:51:00.000+08:002015-10-02T16:26:00.036+08:00Partial function callThis post is part of the <a href="http://mrkschan.blogspot.hk/search/label/pyfun">pyfun</a> series, I will try to *log* some of the features that I think they make Python funny :)<br />
<br />
Again, I was reading the Scala tutorial and find that she has built-in support of partial function call (see <a href="http://twitter.github.io/scala_school/basics.html#functions">http://twitter.github.io/scala_school/basics.html#functions</a>). This reminded me that Python does also has <a href="http://is.gd/csr6gN">functools.partial()</a>, which<a href="http://is.gd/csr6gN"></a> can be used as function shortcuts. <br />
<br />
Let's see this example of Django.<br />
<br />
<pre class="gist">
# Let's have a Coupon that can either be fixed amount discount or percentage off
# But, we don't want to have model inheritance and table join to get the data
class Coupon(models.Model):
code = models.CharField(max_length=8)
type = models.CharField(max_length=11, choices=['fixedamount', 'percentage'])
amount = models.DecimalField(max_digits=8, decimal_places=2)
currency = models.CharField(max_length=3, choices=['USD', 'CAD'], default='')
# To create a Coupon based on certain conditions, you can have this
kwargs = {'code': code}
if condition_a:
kwargs.update({'type': 'fixedamount', 'currency': 'USD'})
elif condition_b:
kwargs.update({'type': 'percentage'})
kwargs.update({'amount': x if condition_c else y})
coupon = Coupon.objects.create(**kwargs)
# Or with functools.partial()
FixedamountCoupon = functools.partial(Coupon.objects.create, type='fixedamount')
PercentageCoupon = functools.partial(Coupon.objects.create, type='percentage')
if condition_a:
coupon = functools.partial(FixedamountCoupon, code=code, currency='USD')
elif condition_b:
coupon = functools.partial(PercentageCoupon, code=code)
coupon = functools.partial(coupon, amount=x if condition_c else y)
coupon = coupon()
</pre>
<br />
Yes, we just shortcuted two types of Coupon using <a href="http://is.gd/csr6gN">functools.partial()</a>, created "sub-class" of Coupon. Furthermore, if the underlying function accepts positional arguments, we can also shortcut those arguments.<br />
<br />
Clean code FTW.<br />
<br />mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-6281778609582961862013-10-11T23:00:00.000+08:002015-10-02T16:26:37.792+08:00Scala Traits in Python?Sometimes, I would question myself, am I qualify as a seasoned Python engineer? The answer is I'm still a junior. The next question is, how to qualify as a seasoned one?<br />
<br />
I have two things in mind that may be part of the answer to the second question:<br />
<br />
1. Know when to use certain Python features.<br />
2. Know what kind of library or framework is suitable for particular task.<br />
<br />
In this <a href="http://mrkschan.blogspot.hk/search/label/pyfun">pyfun</a> series, I will try to *log* some of the features that I think they make Python funny :)<br />
<br />
Let's get back to the primary subject of this post. Scala, which compiles to JAVA bytecode and runs on JVM, is one of the latest big hit. So, I'm going through some tutorials of it to check out her magics. One of the topic is Traits.<br />
<br />
Traits, is similar to Java interface. When I read some samples about it, I wonder how can it be done in Python. From stackoverflow, <a href="http://stackoverflow.com/q/6240118">http://stackoverflow.com/q/6240118</a>, there is a suggestion that to use Python class to simulate the thing. And, starting from that idea, I wonder can we do that in runtime instead of just static definition. Here, we come to Python type().<br />
<br />
Most of the time, type() can be used as Enum in C (see <a href="http://stackoverflow.com/a/1695250">http://stackoverflow.com/a/1695250</a>), or dynamic objects like those in Javascript (<a href="https://gist.github.com/mrkschan/6936112">https://gist.github.com/mrkschan/6936112</a>). It is because type() <b>is essentially a dynamic form of the class statement</b> (<a href="http://docs.python.org/2/library/functions.html#type">http://docs.python.org/2/library/functions.html#type</a>). In other words, we can use type() to create Python class that simulates Traits (JAVA interface) at runtime.<br />
<br />
Here is an example.<br />
<br />
<pre class="gist">
# See original Scala version at http://twitter.github.io/scala_school/basics.html#trait
Car = type('Car', (object,), {'brand': ''})
Shiny = type('Shiny', (object,), {'refraction': 0})
BMW = type('BMW', (Car, Shiny,), {'brand': 'BMW', 'refraction': 100})
my_bmw = BMW()
print my_bmw.brand, my_bmw.refraction
# We can have constructor as well
def bmw_init(self, refraction):
self.refraction = refraction
BMW = type('BMW', (Car, Shiny,), {'brand': 'BMW', '__init__': bmw_init})
c1, c2 = BMW(10), BMW(100)
print c1.refraction, c2.refraction
</pre>
<br />
As we can see, we can use type() to create a set of "interfaces". And, use type() to create class that implement the "interface". These "interfaces" can have its name changed according to runtime conditions. type() is Python magic and it's fun :)<br />
<br />
However, I did not come up with a use case for runtime defined Traits yet :Pmr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com1tag:blogger.com,1999:blog-6721071264063477716.post-68480722204457683082013-09-28T11:59:00.000+08:002013-09-28T11:59:47.308+08:00Bypassing (Great) firewall to access GitHub / BitBucket via SSH TunnelSometimes, you may be blocked by a firewall and cannot access GitHub / BitBucket. In this post, the steps to bypass the firewall using SSH tunnel is documented.<br />
<br />
<br />
Step 1 - Setup the tunnel<br />
----------------------------------------<br />
<br />
Assuming you use SSH to perform git operations (git clone, fetch, pull, merge, etc.), you should find a SSH URL like: <span style="font-family: "Courier New",Courier,monospace;">git@github.com:example/example.git</span> or <span style="font-family: "Courier New",Courier,monospace;">git@bitbucket.org:example/example.git</span>. In order to access the blocked SSH hosts, we have to SSH tunnel to forward the requests. To do so, use the following commands to create a tunnel (assume you have a SSH host that can be accessed).<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">ssh -C -L 8022:github.com:22 example@example.com # Establish a tunnel to github.com, SSH requests to local port 8022 are forwarded to github.com:22.</span><br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">ssh -C -L 8122:bitbucket.org:22 example@example.com # Establish a tunnel to bitbucket.org, SSH requests to local port 8122 are forwarded to bitbucket.org:22.</span><br />
<br />
Of course, you can combine the two to have one SSH session only.<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">ssh -C -L 8022:github.com:22 -L 8122:bitbucket.org:22 example@example.com</span><br />
<br />
<br />
Step 2 - Config SSH client<br />
------------------------------------------<br />
<br />
After you have your tunnels, you can then configure your SSH client to redirect SSH requests to your tunnels. Put the following lines in your ~/.ssh/config file.<br />
<br />
<span style="font-family: "Courier New",Courier,monospace;">Host github.com<br />HostName 127.0.0.1<br />Port 8022<br /><br />Host bitbucket.org<br />HostName 127.0.0.1<br />Port 8122</span><br />
<br />
<br />
Afterwards, feel free to use <span style="font-family: "Courier New",Courier,monospace;">git clone git@github.com:example/example.git</span> or <span style="font-family: "Courier New",Courier,monospace;">git clone git@bitbucket.org:example/example.git</span> (you can also use git fetch, pull, merge, etc.). All requests will be passing through your SSH tunnel.mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com2tag:blogger.com,1999:blog-6721071264063477716.post-71935316329827585212013-04-16T09:59:00.000+08:002013-04-16T09:59:37.563+08:00中產工種被誰取代昨天在看一篇評論,說美國總統做什麼都改變不了大部分中產階層的命運。理據如下:<br />
<br />
1. 大學生普及化,一般水平的人力資源充足,而需求卻沒有相對增加,使一般中產階級工種薪酬下降,而且惡性循環不斷。<br />
<br />
2. 軟件業發達,企業軟件取蒂中產階級工種人手(如中級管理人員),高層工作效率因軟件發達而不斷上升,促使人手需求不斷下降。<br />
<br />
那麼,要擺脫那惡性循環,除了是優秀人才,或是專業工作從業者,或是成功startup的核心團員。還有什麼正常途徑?扎鐵吧!<br />
<br />
作為軟件行業從業員, 又如何擺脫厄運?mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-85114726781722350042013-03-25T11:31:00.002+08:002013-03-25T11:31:21.873+08:00Laptop火牛點解咁大隻Laptop本來的設計精要在於移動性,<br />
<div id=":i2">
<wbr></wbr>但係好多laptop的火牛,都差不多是一罐可樂咁重(<wbr></wbr>最近習慣了用可樂做單位)。帶laptop出街 ,本來是1.5kg卻變了2kg,實在無耐。<br /><br />點解火牛要咁大隻,唔可以好似Apple果D維他奶size?<br /><br />我有谂過買多一隻牛,公司一隻屋企一隻,<wbr></wbr>咁就少左個藉口唔帶Laptop,<wbr></wbr>但又不想為一部舊laptop投入過多金錢… 你話人係不係總要犯賤?又要解決問題又不肯付出:P 還是多花一兩舊水,把懶惰的藉口抹去吧啦</div>
mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com1tag:blogger.com,1999:blog-6721071264063477716.post-76138061867914504042013-03-08T16:13:00.001+08:002014-02-09T16:30:45.038+08:00SEO Tips: Abuse GithubNOTE: I'm not a SEO guy. If this post used wrong wordings, sorry for that. And, I'm not a native English speaker, don't blame my English writing :P And, I don't know whether someone has shared something similar :)<br />
<br />
I have several pet projects hosted on github. And, I observed that Google give pretty high ranking to github repository. If a github repository has a homepage, that homepage will get benefits as well.<br />
<br />
Thus, I carried out an experiment at <a href="https://github.com/mrkschan/github-seo-effect">https://github.com/mrkschan/github-seo-effect</a>. I used the search terms "Github SEO effect" as the repository name and set the homepage that links to my blog post at <a href="http://mrkschan.blogspot.hk/2013/03/github-seo-effect.html">http://mrkschan.blogspot.hk/2013/03/github-seo-effect.html</a><br />
<br />
The result is pretty amazing. Here is a Google search result before I carried out the experiment (I was using Chromium private browsing, with links set to use "gl=us").<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://1.bp.blogspot.com/-gWZM76iJ_LI/UTmc6PoS6dI/AAAAAAAAAtM/4pH8jOZK3BM/s1600/seo-effect-at-2013-03-01T13:23:49.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://1.bp.blogspot.com/-gWZM76iJ_LI/UTmc6PoS6dI/AAAAAAAAAtM/4pH8jOZK3BM/s320/seo-effect-at-2013-03-01T13:23:49.png" height="177" width="320" /></a></div>
My blog post gets nowhere on the search result.<br />
<br />
After a few days that I set the homepage to the blog post, the result becomes the following screencap.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="http://2.bp.blogspot.com/-k9a8MwMxLoo/UTmdPl7o8aI/AAAAAAAAAtU/FB8xryjV3jw/s1600/seo-effect-at-2013-03-08T15:01:29.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="http://2.bp.blogspot.com/-k9a8MwMxLoo/UTmdPl7o8aI/AAAAAAAAAtU/FB8xryjV3jw/s320/seo-effect-at-2013-03-08T15:01:29.png" height="179" width="320" /></a></div>
My repository goes to the top of the search result. And, the blog post is on the first page of the Google Search :)<br />
<br />
BTW, shall we abuse Github for SEO purpose? Try your search here: <a href="http://www.google.com/search?gl=us&q=github+seo+effect">http://www.google.com/search?gl=us&q=github+seo+effect</a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.comtag:blogger.com,1999:blog-6721071264063477716.post-80420282521690862672013-03-01T13:18:00.001+08:002014-02-09T16:30:37.188+08:00Github SEO effectThis is a blog post to experiment the hypothesis:<br />
<br />
Google would give github repo pretty high ranking. <br />
<del>If a repo's README has a link to a page outside github, that page gets SEO bonus.</del>edit: 20130308 If a repo's homepage has set to a web page, that page
would get SEO bonus from Google (I didn't carry out experiment on the
effect of the README yet). And, if the page links back to the github
repo, that's a plus. <br />
<br />
Search google with the query "Github SEO effect" :)<br />
<br />
Link: <a href="https://github.com/mrkschan/github-seo-effect" target="_blank">https://github.com/mrkschan/github-seo-effect </a><br />
<br />
Result: <a href="http://www.google.com/search?gl=us&q=github+seo+effect">http://www.google.com/search?gl=us&q=github+seo+effect</a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.comtag:blogger.com,1999:blog-6721071264063477716.post-17845243858701532702013-02-28T22:06:00.003+08:002016-03-03T02:40:14.275+08:00信念, Faith男人問女人: "你有什麼信念嗎?"<br />
<br />
女人問: "為什麼問這個問題?"<br />
<br />
男人說: "別人說人不能沒有信念."<br />
<br />
女人答: "我相信會找到一個愛我的人."<br />
<br />
女人問: "那你呢?"<br />
<br />
男人說: "我不知道."<br />
<br />
朋友們, 你的信念是什麼?<br />
<br />
<br />
Faith - (from thefreedictionary.com)<br />
<div class="ds-list">
<b>1. </b> Confident belief in the truth, value, or trustworthiness of a person, idea, or thing.</div>
<b>2. </b> Belief that does not rest on logical proof or material evidence.<br />
<br />
信念 - (from 中華民國教育部 - 重編國語辭典修訂本)<br />
信仰不疑的意念.<br />
<br />
<br />
我的信念? 可能是建立一個舒適的家 :)mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-59804219084640430302013-01-31T22:05:00.001+08:002016-03-03T02:39:46.027+08:00很久沒寫最近寫的, 不是生活相關的, 就是技術相關的, 缺少了思考的東西. 人像懶了, 笨了.<br />
<br />
原因? 曾經想過會不會是生活太匆忙而給自己藉口不去思考東西/反思. 又會不會是習慣了 twitter 文化, 一切從簡? 那今天又為什麼在寫這篇廢話呢? 不是在 twitter 上寫句: o, i'm so lazy. 就行了嗎?<br />
<br />
事原: 有位家人最近離開了這個世界, 我因而接觸到佛學的地藏王菩薩經. 裡面, 提到了無間地獄. 無間地獄在電影無間道被提及過: "明明我已晝夜無間踏盡面前路, 夢想中的彼岸為何還未到". 再加上最近有點迷失(even after i read the book "Life is what you make it" twice), 對無間地獄提起了點興趣. 剛坐車回家的時候, 在想如何去認識無間地獄. 忽然, 想起了在大學的時候, 學習道家文化時在這個 blog 寫了點東西. 到家了, 便走上這裡, 找找以前寫的<a href="http://mrkschan.blogspot.hk/2006/11/blog-post_19.html" target="_blank">東西</a>. 期間, 我在看自己以前寫的東西. 整個過程很有趣. 因為我在嘗試認識從前的我. 看了, 發覺對從前的我很陌生, 而且這個過程很有趣 :)<br />
<br />
為此有感而發, 在twitter文化驅使下寫了句 "regrets should be prevented, but reviewed is encouraged. <span class="invisible">http://</span><span class="js-display-url">mrkschan.blogspot.hk/2006/09/blog-p</span><span class="invisible">ost.html</span><span class="tco-ellipsis"><span class="invisible"> </span>…</span> my 1st blog :)". review的就是要多認識自己, regret的就是我的<span class="short_text" id="result_box" lang="zh-TW"><span title="灵魂">靈魂</span></span>好像有一年時間消失了一樣.<br />
<br />
想了一會, 我不想以後再後悔. 花少少時間, 寫一寫, 裝個讀書人.<br />
<br />
<br />mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-22955100955317585312012-12-20T14:33:00.001+08:002012-12-25T10:26:32.208+08:00How to use Shovel with DjangoHere is my attempt to use <a href="https://github.com/seomoz/shovel" target="_blank">Shovel (Rake, for Python)</a> in Django.<br />
<br />
Shovel can be used as an alternative to Django built-in Command. The reasons to use Shovel instead of the Command are:<br />
<br />
1. "Rake, for Python" sounds cool.<br />
2. Shovel is light-weight.<br />
3. I can find all tasks in one place (the shovel/ directory).<br />
4. I hate writing a list of make_option.<br />
5. I hate inheriting LabelCommand, BaseCommand, NoArgsCommand, etc.<br />
<br />
Okay, you get it... The reasons to use Shovel are actually: (i) "Rake, for Python" sounds cool, and (ii) I am lazy.<br />
<br />
I didn't use the term "replacement for Django Command" because we cannot simply invoke Shovel task in the Django codebase using django.core.management.call_command(). (But we can have work-around, like making a Command to proxy the call)<br />
<br />
Let's get back to my attempt to mix the two.
I placed the shovel/ folder inside <a href="https://github.com/mrkschan/shovel-django-mix/tree/master/themix" target="_blank">the Django project</a>.<br />
<br />
<pre class="gist">
djangoproject/
- manage.py
- settings.py
- urls.py
- shovel/
</pre>
<br />
And I setup the Django environment at the <a href="https://github.com/mrkschan/shovel-django-mix/blob/master/themix/shovel/thingscli.py#L3" target="_blank">top of the Shovel task file</a>.
<pre class="gist">
try: # Load the django environment
import imp
import os
import sys
me = os.path.abspath(os.path.dirname(__file__))
module_info = imp.find_module('context', [me])
imp.load_module('context', *module_info)
except:
print >> sys.stderr, 'Cannot setup Python environment from context.py'
</pre>
At last, <a href="https://github.com/mrkschan/shovel-django-mix/blob/master/themix/shovel/context.py" target="_blank">the setup</a> injects a directory to Python sys.path and use Django function to load the settings.py.
<pre class="gist">
def setup_django_env(path):
from django.core.management import setup_environ
try:
module_info = imp.find_module('settings', [path])
settings = imp.load_module('settings', *module_info)
setup_environ(settings)
except ImportError:
msg = "Error: Can't find 'settings.py' in configured PYTHON_PATH", path
print >> sys.stderr, msg
sys.exit(1)
# assume shovel/ directory is placed at the same level of settings.py
setup_django_env(os.getcwd())
</pre>
This is a common technique to integrate Django with other Python gears as well. By applying this technique, I can freely use any Django functions / models in Shovel.<br />
<br />
NOTE 1: The name of Shovel task file cannot collide with any module/app name of the Django project.<br />
<br />
NOTE 2: We can place the shovel/ folder outside the Django project but we have to use proper Python path and import statement (e.g. use `from themix.things.models import Thing` instead of `from things.models import Thing`). <br />
<br />
My attempt can be found at - <a href="https://github.com/mrkschan/shovel-django-mix" target="_blank">https://github.com/mrkschan/shovel-django-mix.</a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-62032949106863410772012-01-20T22:38:00.000+08:002012-12-20T14:05:33.455+08:00git-fix-whitespace series 1: Knowing about `git diff -p`In the <a href="http://mrkschan.blogspot.com/2012/01/git-fix-whitespace-series-0-gitpython.html">last post</a>, the first requirement of the project (<a href="https://github.com/mrkschan/git-fix-whitespace">https://github.com/mrkschan/git-fix-whitespace</a>) has been settled. The next requirement of this project is to read the git-diff patch in order to find any line changes that violate the whitespace rules specified in git config.<br />
<br />
In a typical git-diff patch (see below), there are a few major parts.<br />
<script src="https://gist.github.com/1647409.js">
</script>
<ol>
<li>Line 1 provides metadata about the modified file. It gives the file path to the file, rooted at the git repository.</li>
<li>Line 2 provides metadata about the git index and the file object discretionary access control list.</li>
<li>Line 3 and 4 provides metadata about which file path is old and which is new.</li>
<li>Line 5, 14, and 24 are metadata that tells which part of the file is modified. Let's take an example to explain this - "@@ -33,8 +33,7 @@ def sanitize_diff(git_diff):". "-33,8" tells that there is a diff hunk with 8 lines starts at line 33 of the old version of the file. "+33,7" tells that there is a diff hunk with 7 lines starts at line 33 of the new version of the file. As a result, the new version of the file gets one line fewer than the old version.</li>
<li>The rest of the patch is the content in the modified file. Those lines are either prefixed by ' ', '-', or '+'. ' ' means no modification, '-' means removed line, and '+' means added line. Note, there is no line replacement since it is represented by '-' lines followed by '+' lines (see line 28-31).</li>
</ol>
After knowing the structure of a git-diff patch, the next step is to read and write the modified file.mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-51420142881983105432012-01-15T18:27:00.003+08:002012-12-20T14:05:33.453+08:00git-fix-whitespace series 0: GitPython vs libgit2This is the first post of the git-fix-whitespace series. In this series, I will put some notes about working on the project - <a href="https://github.com/mrkschan/git-fix-whitespace">https://github.com/mrkschan/git-fix-whitespace</a>. (NOTE: This series is a by-product of the git-fix-whitespace project, since my blog need some updates :P)<br /><br />First of all, let me introduce the rationale of working on this git-fix-whitespace project. Most of the time, as a Python developer, I hate tab indentation and trailing whitespaces (read pep8). I know there are existing tools to "proof-read" a file using certain whitespacing rules. However, I insist to create my own tool to achieve the goal. Reason: I just wanna have a pet project that I LOVE to keep working on.<br /><br />The very first requirement of this project is to support the configuration directives of git (see `man git-config` and look for `core.whitespace`). Hence, I need to find a tool that can read the git configuration files (both the user level git config file ~/.gitconfig and repository level git config file <repo>/.git/config). By using Google, I got GitPython and libgit2/pygit2. At first, I try to read the libgit2 python binding to see if I can read whitespace configuration by simple api calls. But it seems that it did not support that yet (as of 2012-Jan-15). Then I move on to GitPython. Gotcha! There's simple api call to read the core.whitespace configurations :) As a result, git-fix-whitespace got a dependency on GitPython at the moment.<br /></repo>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-80490268655971497562011-11-05T21:32:00.000+08:002011-11-05T21:32:13.416+08:00話說的好滑腸粉聽阿媽話說, 97年果時有位茶樓點心大廚, 因為人工高被人炒魷, 結果到了沙田博康村的大排檔拉腸粉.
今日, 本來是要跟阿媽行山去川龍飲茶的日子, 但因為唔多想坐車入荃灣咁遠, 結果是從獅子山行入沙田...
朝早7:30出門口, 9:30到了博康村. 剛剛好趕得上腸粉時段 (NOTE: 果間野 d 即拉腸粉, 只限每天早上十時前供應).
<div class="separator" style="clear: both; text-align: center;">
<a href="http://3.bp.blogspot.com/-fffu21rR7Go/TrU5z5s0GiI/AAAAAAAAApg/eWfC0-Uj0aI/s1600/DSC00624.JPG" imageanchor="1" style=""><img border="0" height="240" width="320" src="http://3.bp.blogspot.com/-fffu21rR7Go/TrU5z5s0GiI/AAAAAAAAApg/eWfC0-Uj0aI/s320/DSC00624.JPG" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<a href="http://4.bp.blogspot.com/-2MB6ExF-3v8/TrU5zs5mMiI/AAAAAAAAApY/n8U9JnjZ108/s1600/DSC00623.JPG" imageanchor="1" style=""><img border="0" height="320" width="240" src="http://4.bp.blogspot.com/-2MB6ExF-3v8/TrU5zs5mMiI/AAAAAAAAApY/n8U9JnjZ108/s320/DSC00623.JPG" /></a></div>
這即拉腸粉的滑口程度, 絕對比米豬蓮添女子運的腸粉好得多... And, 添女子運沒有花生醬及甜醬供應....mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-84887903002588962512011-05-12T23:26:00.001+08:002011-05-25T15:21:56.652+08:00Stats for Firefox 4 Release Party on May 14As you may know, we're going to have a Firefox 4 Release Party this Saturday in Hong Kong!! (see <a href="http://opensource.hk/node/666">http://opensource.hk/node/666</a> :) <br /><br />I would like to share with you some interesting stats we collected from the registration :)<br /><br />At first, let's see who's coming :)<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-kDAsq0s3Y4k/Tcv8jlAo1GI/AAAAAAAAAlo/qIY_p9e42FA/s1600/fx4-who.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 70px;" src="http://4.bp.blogspot.com/-kDAsq0s3Y4k/Tcv8jlAo1GI/AAAAAAAAAlo/qIY_p9e42FA/s320/fx4-who.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5605851849353122914" /></a><br /><br />Most of us are user of Firefox :) You can find contributor as well :)<br /><br />Then, which OS we use most???<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-A2vMNkaddRg/Tcv8jGvzKAI/AAAAAAAAAlY/k1yrvG-ekuE/s1600/fx4-os.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 85px;" src="http://1.bp.blogspot.com/-A2vMNkaddRg/Tcv8jGvzKAI/AAAAAAAAAlY/k1yrvG-ekuE/s320/fx4-os.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5605851841229432834" /></a><br /><br />Feeling sorry to "vista" ... what is it -.-? Anyway, what are we going to do this Saturday ?? hehe... see below :)<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-omPW5S6fG6c/Tcv8jRapl7I/AAAAAAAAAlg/Xg2HflLdjOk/s1600/fx4-role.png"><img style="cursor:pointer; cursor:hand;width: 320px; height: 72px;" src="http://4.bp.blogspot.com/-omPW5S6fG6c/Tcv8jRapl7I/AAAAAAAAAlg/Xg2HflLdjOk/s320/fx4-role.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5605851844093515698" /></a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-84093062224976519392011-02-14T21:13:00.002+08:002011-02-14T21:18:09.862+08:00happy valentine's<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-fx8-ZSK7lCc/TVkrH_aNmCI/AAAAAAAAAk8/rWg5kJGnMXA/s1600/DSC00514.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://4.bp.blogspot.com/-fx8-ZSK7lCc/TVkrH_aNmCI/AAAAAAAAAk8/rWg5kJGnMXA/s320/DSC00514.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5573533430128154658" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/-ksrpN_R-NlQ/TVkrHgaoyhI/AAAAAAAAAk0/fg_j2t2ey10/s1600/DSC00511.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://2.bp.blogspot.com/-ksrpN_R-NlQ/TVkrHgaoyhI/AAAAAAAAAk0/fg_j2t2ey10/s320/DSC00511.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5573533421808437778" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/-ktzUh1T75VY/TVkrHX6FGfI/AAAAAAAAAks/lCosxLCBazw/s1600/DSC00515.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://2.bp.blogspot.com/-ktzUh1T75VY/TVkrHX6FGfI/AAAAAAAAAks/lCosxLCBazw/s320/DSC00515.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5573533419524397554" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-a2vXfhVpzUQ/TVkrHBGdTII/AAAAAAAAAkk/6uYk0Kq62_s/s1600/DSC00515.masked2.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://1.bp.blogspot.com/-a2vXfhVpzUQ/TVkrHBGdTII/AAAAAAAAAkk/6uYk0Kq62_s/s320/DSC00515.masked2.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5573533413402299522" /></a><br /><br /><a href="http://mrkschan.blogspot.com/2011/02/prep-for-monday.html">http://mrkschan.blogspot.com/2011/02/prep-for-monday.html</a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-48796235522316481632011-02-12T12:28:00.013+08:002011-02-12T13:13:06.750+08:00屎忽痕, 行山飲"早"茶昨晚, 阿爸忽然話今朝行山去什麼 "川龍" 飲早茶.. 小弟見屎忽痕, 又 assume 了要行山去吃的 實會係正野 ... 很自然的話要跟住去.<br /><br />今晨七時出門口 .. 坐mtr到荃灣, 由荃灣地鐵站行上圓玄學院. (那時大約是8點了)<br /><br />由圓玄學院出發, 經山路到川龍大約為 3Km. 不要以為很輕鬆... 從圓玄學院走上大帽山的樓梯, 約一公里長 -.- 跟阿媽慢慢走了40分鐘, 才到那什麼觀景台.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://2.bp.blogspot.com/-CGo0iidcDN0/TVYOVAVhcyI/AAAAAAAAAjY/1RhwpqVBvsM/s1600/DSC00518.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://2.bp.blogspot.com/-CGo0iidcDN0/TVYOVAVhcyI/AAAAAAAAAjY/1RhwpqVBvsM/s320/DSC00518.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572657342947881762" /></a><br /><br />之後再走一會輕鬆的山路, 就差不多到川龍了.<br /><br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-ITHgBaMOY8k/TVYQIUhHn6I/AAAAAAAAAjg/Y2mDXoodErI/s1600/DSC00521.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-ITHgBaMOY8k/TVYQIUhHn6I/AAAAAAAAAjg/Y2mDXoodErI/s320/DSC00521.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572659324050186146" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-_2OwQzV0Gto/TVYQJKJoHfI/AAAAAAAAAjo/1Nti0A5ym4M/s1600/DSC00524.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-_2OwQzV0Gto/TVYQJKJoHfI/AAAAAAAAAjo/1Nti0A5ym4M/s320/DSC00524.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572659338447166962" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-A015LBK_EQg/TVYQJfrLyXI/AAAAAAAAAjw/FD-oQAMcAZE/s1600/DSC00525.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-A015LBK_EQg/TVYQJfrLyXI/AAAAAAAAAjw/FD-oQAMcAZE/s320/DSC00525.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572659344225061234" /></a><br /><br />上到茶樓, 見到好耐無見的雀籠.. 對上一次與雀共茶, 應該係細路仔果時, 同屋企人去藍田的德田村冬菇亭了. 自從有禽流感之後, 都好耐無試過聽雀仔聲飲茶.<br /><br />在茶樓叫的東西不是很多, 其中的芋角同客家豬肉最為正 :P 那個芋角入面, 有d馬蹄, 很爽口. 而那個豬肉, 不是很肥. 豬肉像三文治一樣, 中間夾了一片芋頭.<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-TztLldaSryQ/TVYSbyTs8II/AAAAAAAAAkQ/39S-80U0IUY/s1600/DSC00528.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://4.bp.blogspot.com/-TztLldaSryQ/TVYSbyTs8II/AAAAAAAAAkQ/39S-80U0IUY/s320/DSC00528.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572661857487745154" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-YXuNLqfZYA8/TVYSbof606I/AAAAAAAAAkI/mHGm742OBso/s1600/DSC00530.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://1.bp.blogspot.com/-YXuNLqfZYA8/TVYSbof606I/AAAAAAAAAkI/mHGm742OBso/s320/DSC00530.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572661854854632354" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/-Nrl1286xh-E/TVYSbdcXmzI/AAAAAAAAAkA/5J0seCq9qlE/s1600/DSC00527.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://1.bp.blogspot.com/-Nrl1286xh-E/TVYSbdcXmzI/AAAAAAAAAkA/5J0seCq9qlE/s320/DSC00527.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572661851886951218" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-NdVLzOUg-DA/TVYSbKG_CyI/AAAAAAAAAj4/vT7vzVN4qCc/s1600/DSC00529.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://4.bp.blogspot.com/-NdVLzOUg-DA/TVYSbKG_CyI/AAAAAAAAAj4/vT7vzVN4qCc/s320/DSC00529.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572661846696987426" /></a><br /><br />三個人.. 70$ 落樓.<br /><br />小記:<br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/-HtjkLnAX14M/TVYTR--kvTI/AAAAAAAAAkY/rugjtLtnHcU/s1600/DSC00531.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://4.bp.blogspot.com/-HtjkLnAX14M/TVYTR--kvTI/AAAAAAAAAkY/rugjtLtnHcU/s320/DSC00531.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5572662788601724210" /></a><br /><br />坐車落荃灣果陣, 經過了曹公潭戶外康樂中心. 我不記得是小學還是中學去過呢到. 最記得係.. 有一晚在裡面不知道干嘛通宵後.. 到了那 canteen 吃早餐... 那個早餐是我一生人入面 (暫時來說) ... 最難食的一餐 -.-"mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com0tag:blogger.com,1999:blog-6721071264063477716.post-10576323965676114082011-02-09T13:33:00.004+08:002011-02-09T13:41:42.969+08:00Prep for Monday.老板有個 lunch meeting, 到了旺角新世紀. 那麼近花墟, lunch time 當然不可放過 :)<br /><br />先到"快靚正"的<a href="http://www.openrice.com/restaurant/sr2.htm?shopid=11293">金華</a>整番個 奶茶 lunch, 再過花墟走走也有大把時間.<br /><br />雖然吃的不是金華出名的波羅油, 但 28$ 快餐 (連湯連奶茶), 在太子也算是抵吧 :P<br /><br />為什麼要到花墟?? 因為快來的星期一吧 :P<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_3MkWQdMW-_A/TVIoxtocadI/AAAAAAAAAjQ/sYW97KCHKg8/s1600/DSC00508.mod.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://3.bp.blogspot.com/_3MkWQdMW-_A/TVIoxtocadI/AAAAAAAAAjQ/sYW97KCHKg8/s320/DSC00508.mod.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5571560523538655698" /></a> (張相影得唔好 :P)<br /><br />預祝情人節快樂 :)mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com1tag:blogger.com,1999:blog-6721071264063477716.post-29425021166293782592011-01-28T12:48:00.011+08:002011-01-28T13:19:25.857+08:00茄牛通很久沒有興致寫野, 難得今日有番些許, 緊係寫番些許東西.<br /><br />今日跟老板去了 中環 Alexandra House間 Starbucks. 老板十一點多就有個 lunch meeting ... 咁我果陣又有些許肚餓 ... 緊係 <a href="http://www.openrice.com/restaurant/sr1.htm?s=1&district_id=1003&inputcategory=cname&inputstrrest=">openrice: 中環</a>.<br /><br />去到第一頁 result, 第一間野竟然係<a href="http://www.openrice.com/special/feature.htm?cmsid=345">酸辣粉</a> ... 無言... 即時 skip 了沒看 -.-... 不想深圳又酸辣粉, 香港又酸辣粉...<br /><br />result 第二間就係今日 lunch <a href="http://www.openrice.com/restaurant/sr2.htm?shopid=10577">目的地</a>... 眼見 less than $40 ... 唔洗諗啦 ... 去馬 :P<br /><br />那時還想在 starbucks 坐多陣, 但又見 review 話要等位, 11:45左右就行出 starbucks, 找路到<a href="http://www.openrice.com/restaurant/sr2.htm?shopid=10577&mapType=1">勝香園</a>.<br /><br />大約 12:05 搵到間野, 果時都唔係"好Q"多人, 雖然有人等緊位, 但見間野仲有一兩張細檯未開. 一條友o既我, 緊係見位就坐 -.- 叫左個 茄牛蛋通 + 熱奶荼 ($25 + $9).<br /><br />叫完阿姐寫野, 唔洗3分鐘就有得食... 果時身後有班女仔係到等位.. 本來想幫碗通粉影番張靚相, 但太多女仔望住.. 淆底, 廢事做柒野 :P<br /><br /><img src="http://static2.openrice.com.hk/UserPhoto/photo/0/1R/000CHLA0A427CF3C0C7F45l.jpg" /><br />(老笠 openrice 其中一張相)<br /><br />講番碗通粉... 其實.... 沒什麼特別... 跟以前(細路仔果陣) d 屋村大排檔 同 屋企 d 住家口味 差不多... 可能大家對那些 大x樂, x快活, 美x 等等 chain 吃厭了味覺... 不知道為什麼會有些遊客"慕名"到訪 (self included). 不過... 很久沒有食粉麵連湯飲... 感覺那碗番茄湯沒多味精呢.<br /><br />有些時候還會覺得, 沒有辨公室的生活, 令人幾嚮往...mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com2tag:blogger.com,1999:blog-6721071264063477716.post-90849618456756187012011-01-21T19:36:00.004+08:002011-01-21T19:46:12.500+08:00Badges Unlocked.現在什麼遊戲, 什麼網站都有 badges / achievements... 今日, 現實生活中 (網絡還不現實麼?) 多了一個 badge ... upload 上來看看的說 :P<br /><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_3MkWQdMW-_A/TTlxuNHjXyI/AAAAAAAAAio/cIT2Lbp6eP4/s1600/DSC00495.JPG"><img style="cursor:pointer; cursor:hand;width: 320px; height: 240px;" src="http://3.bp.blogspot.com/_3MkWQdMW-_A/TTlxuNHjXyI/AAAAAAAAAio/cIT2Lbp6eP4/s320/DSC00495.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5564603853202218786" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://1.bp.blogspot.com/_3MkWQdMW-_A/TTlxt9hGSKI/AAAAAAAAAig/pjokAUxmG9E/s1600/DSC00496.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://1.bp.blogspot.com/_3MkWQdMW-_A/TTlxt9hGSKI/AAAAAAAAAig/pjokAUxmG9E/s320/DSC00496.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5564603849014397090" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://3.bp.blogspot.com/_3MkWQdMW-_A/TTlxtWX-_4I/AAAAAAAAAiY/eaURsPUxCBo/s1600/DSC00497.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://3.bp.blogspot.com/_3MkWQdMW-_A/TTlxtWX-_4I/AAAAAAAAAiY/eaURsPUxCBo/s320/DSC00497.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5564603838507188098" /></a><br /><a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://4.bp.blogspot.com/_3MkWQdMW-_A/TTlxtCZUm5I/AAAAAAAAAiQ/7LzWe8RpHW8/s1600/DSC00499.JPG"><img style="cursor:pointer; cursor:hand;width: 240px; height: 320px;" src="http://4.bp.blogspot.com/_3MkWQdMW-_A/TTlxtCZUm5I/AAAAAAAAAiQ/7LzWe8RpHW8/s320/DSC00499.JPG" border="0" alt=""id="BLOGGER_PHOTO_ID_5564603833144089490" /></a>mr.kschanhttp://www.blogger.com/profile/06012690204954889945noreply@blogger.com1