Prosty automat skończony (FSM) w Python

Na GitHub i PyPi wrzuciłem implementację prostego automatu skończonego https://github.com/marcinn/dsm

Instalacja

pip install dsm

Przykład 1: Stany zamówienia w sklepie

Załóżmy, że mamy do oprogramowania automat stanów zamówienia w sklepie:

Deklaracja

Wystarczy zdefiniować przejścia za pomocą listy krotek ([stan], [wartość], [nowy stan]), aby automat spełniał swoją rolę.

Deklaracja automatu z przykładu wygląda następująco:

import dsm

class OrderFSM(dsm.StateMachine):
    class Meta:
        transitions = (
            ('new', 'accept', 'accepted'),
            ('new', 'cancel', 'cancelled'),
            ('accepted', 'mark_ready', 'ready'),
            ('accepted', 'cancel', 'cancelled'),
            ('ready', 'send', 'sent'),
            ('ready', 'cancel', 'cancelled'),
            ('sent', 'finalize', 'finalized'),
        )
        initial = 'new'

Symulacja

Kod symulacji:

fsm = OrderFSM()

print 'State: `%s`, trying to accept' % fsm.state

fsm.process('accept')
print 'State: `%s`' % fsm.state

print "Can I send order now? - %s." % ('Yes' if fsm.can('send') else 'No')
print 'Marking as ready.'
fsm.process('mark_ready')

print "Can I send order now? - %s." % ('Yes' if fsm.can('send') else 'No')

fsm.process('send')
print "Order is %s" % fsm.state

print "Trying to cancel order..."
fsm.process('cancel')

Wynik:

State: `new`, trying to accept
State: `accepted`
Can I send order now? - No.
Marking as ready.
Can I send order now? - Yes.
Order is sent
Trying to cancel order...
Traceback (most recent call last):
  File "bin/python", line 78, in <module>
    exec(compile(__file__f.read(), __file__, "exec"))
  File "dsmtest.py", line 39, in <module>
    fsm.process('cancel')
  File "/home/marcin/src/projekty/archikat/eggs/dsm-0.2-py2.7.egg/dsm.py", line 97, in process
    new_state = self._transitions.execute(value, self.state)
  File "/home/marcin/src/projekty/archikat/eggs/dsm-0.2-py2.7.egg/dsm.py", line 46, in execute
    raise UnknownTransition('Can not find transition for `%s` in state `%s`' % (value, current_state))
dsm.UnknownTransition: Can not find transition for `cancel` in state `sent`

Użycie z Django

Moduł nie ma żadnych zależności od Django, ale można go łatwo zintegrować z modelem. Na przykład tak:

class Order(models.Model):
    status = models.CharField(max_length=32)

    def change_status(self, operation):
        # process() zwraca nowy stan
        # nowa instancja OrderFSM() zapewnia prawidłowy stan początkowy
        self.status = OrderFSM(initial=self.status).process(operation) 

Wywołanie zmiany stanu zamówienia może być następujące:

order = Order.objects.get(pk=666)

try:
    order.change_status('send')
except OrderFSM.UnknownTransition:
    print "Buuu... :("

Inne zastosowania

W module dsm.py zawarłem przykład automatu sumującego wprowadzane na wejściu cyfry jako przykład użycia w innych celach niż tylko manipulacja stanami jakichś tam dokumentów. DSM emituje zdarzenia i dzięki możliwości rejestrowania callbacków można zbudować coś znacznie ciekawszego.

Przykład 2: Parzysta ilość zer w ciągu

Jako kolejny przykład zaimplementowałem automat sprawdzający, czy liczność zer w ciągu wejściowym jest parzysta, opisany na Wikipedii.

import dsm

class ParityChecker(dsm.StateMachine):
    class Meta:
        transitions = (
                ('even', '0', 'odd'),
                ('even', '1', 'even'),
                ('odd', '1', 'odd'),
                ('odd', '0', 'even'),
            )
        initial = 'even'


pc = ParityChecker()


while True:
    pc.reset()

    digits = raw_input('Podaj ciag zlozony z zer i jedynek: ')

    if not digits:
        break

    pc.process_many(digits)

    print "%sparzysta ilos zer (%s)" % ('Nie' if pc.state=='odd' else '',
            digits.count('0'))

RESTful JavaScript client w kwadrans

Próbowałem restful.js, próbowałem jQuery REST client, aż ostatecznie dałem sobie spokój. Dla mnie były jakieś trudne/ciężkie w użyciu. A czym powinien być REST client? Cienkim wrapperem na dowolnego klienta HTTP, który gada z dowolnym url-em.

Na własne potrzeby założyłem, że:

  • interfejs ma być banalnie prosty w użyciu
  • wystarczy obsługa tylko content type application/json
  • ma być obsługa nagłówków, także defaultowych (żeby się nie powtarzać)
  • dozwolona jest zależność od jQuery (można się tego względnie łatwo wyzbyć)

Powstał prototyp:

var RestAPI = function(url, params) {
    var absoluteUrlPattern = /^https?:\/\//i,
        self=this,
        params = params || {};

    this.defaultHeaders = params.headers || {};
    this.url = url;

    function isAbsoluteUrl(url) {
        return absoluteUrlPattern.test(url);
    }

    function resourceToUrl(resource) {
        resource = resource || '';
        if(isAbsoluteUrl(resource)) {
            return resource;
        } else {
            return self.url+'/'+resource;
        }
    }

    function makeResponseObject(jqXhr, data) {
        var headers = {},
            headersList = jqXhr.getAllResponseHeaders().split('\r\n');
        $.each(headersList, function(i, headerStr) {
            var headerTuple = headerStr.split(': ');
            if(headerTuple[0]) {
                headers[headerTuple[0].toLowerCase()] = headerTuple[1];
            }
        });
        return {
            status: jqXhr.status,
            statusText: jqXhr.statusText,
            headers: headers,
            data: data 
        };
    }

    function doMethod(type, resource, data, headers) {
        var dfr = $.Deferred(),
            completeHeaders = $.extend({}, self.defaultHeaders, headers);

        $.ajax({
            url: resourceToUrl(resource),
            type: type,
            dataType: 'json',
            contentType: 'application/json',
            headers: completeHeaders,
            data: typeof(data)!=='undefined' && data!==null ? JSON.stringify(data) : null
        }).then(function(respData,respStatus,jqXhr) {
            dfr.resolve(respData, makeResponseObject(jqXhr, respData));
        }, function(jqXhr) {
            dfr.reject(makeResponseObject(jqXhr));
        });
        return dfr.promise();
    };


    this.get = function(resource, params, headers) {
        return doMethod('GET', resource, params, headers);
    }

    this.post = function(resource, payload, headers) {
        return doMethod('POST', resource, payload, headers);
    }

    this.put = function(resource, payload, headers) {
        return doMethod('PUT', resource, payload, headers);
    }

    this.patch = function(resource, payload, headers) {
        return doMethod('PATCH', resource, payload, headers);
    }

    this['delete'] = function(resource, headers) {
        return doMethod('DELETE', resource, null, headers);
    }

}

Działa? Działa, i to ciekawie.

Przykład - kto mnie śledzi na GitHub?

var github = new RestAPI('https://api.github.com');

github.get('users/marcinn').then(function(user) {
    github.get(user.followers_url).then(function(followers) {
        console.log(followers);
    });
});

Koniec, kropka. Prosto i na temat. Pobawta się sami na plunkr.

Jak kogo interesuje - publikuję na licencji "Brać! Mać..." oczywiście bez żadnej gwarancji. Wspomnijta w creditsach, że był taki Nowak, któremu to się klepać kodu nie chciało.

I jeszcze jedno - skoro się niektórzy chwalą filesize, to ja też podam:

  • Original: 2370 bytes
  • Gzipped: 764 bytes

Niech przebijają.


A AngularJS i jego ngResource albo nawet Restangular? Głupotą jest mapowanie REST na CRUD (albo stosowanie active record). W REST mamy dobrze określone VERBS. Nie ma wśród nich save. I tyle w temacie.

REST po ludzku

Znalazłem ostatnio dwa ciekawe wpisy traktujące o architekturze REST. Pierwszy z nich jest autorstwa niezastąpionego Martina Fowlera, który ciekawie przedstawił drogę od prostych usług typu RPC aż po podstawy RESTful (ale nie samego REST-a). Zdecydowanie ułatwia przejście od jednej do drugiej koncepcji: http://martinfowler.com/articles/richardsonMaturityModel.html

Z kolei drugą perełką jest wpis Ryana Tomayko, programisty GitHuba i Heroku, który wytłumaczył idee RESTful (wirtualnej?) żonie. Niestety środowisko gender zaatakowało Ryana i autor usunął artykuł-wywiad, ale na szczęście (w tym wypadku) Internet zachował jego kopie. Znajdziecie takową m.in. na archive.org: http://web.archive.org/web/20130116005443/http://tomayko.com/writings/rest-to-my-wife

Miłej lektury!

Trochę o nowościach

Jestem wprost zafascynowany i rozpływam się jak masło w rondlu, kiedy otrzymuję zamówione newslettery. Jest spora szansa na to, że podobnie mają inni ludzie - zawodowcy, pasjonaci, hobbyści. Może są to nawet cholerne tysiące ludzi, a biorąc pod uwagę prasę drukowaną to i miliony istnień, które chcą dowiedzieć się co się w danym środowisku, produkcie czy dziedzinie nauki dzieje, a co jest planowane i wkrótce się wydarzy. Przyjęło się, że czytamy newsy, u Putina новости czy też известие, a po naszemu nowości a nawet niusy, choć właściwie neologizmy mnie drażnią. Toteż towarzyszy tym newsletterom pewien stopień podniecenia, doprowadzają mnie one niemal do rozkoszy, aby w końcu ten jeden szczególny - od Polish Python Coders Group - działał jak kubeł zimnej wody. Albo nie - jak przegnanie człowieka na golasa przez wartką, górską rzekę o piątej nad ranem.

Nowości, to są rzeczy i wydarzenia nowe, nadchodzące, naturalnie w opozycji do przeszłości, czasów zamierzchłych i zdarzeń minionych. I do prawdy nie mogę zrozumieć dlaczego z uporem maniaka PPCG rozsyła "nowości" z przeszłości. Tak! Są to zamierzchłości publikowane jako nowości, tytułowane przewrotnie "newsletterem" (list z nowościami, jakby nie patrzeć), z których dowiedzieć się możemy, że "za dwa dni było spotkanie koderów Pythona w Osrajdowie Małym" albo że "w minionym miesiącu będzie można wziąć udział w konkursie".

Kilkakrotnie pisałem na adres e-mail podany w newsletterze, ale po braku reakcji wnoszę, iż prawdopodobnie lądują one zawsze w przyszłości i nigdy nie zostaną odczytane, chyba że ktoś w przyszłości zorientuje się, że rozsyłali informacje z przeszłości, i wówczas może nastąpić jakieś załamanie czasoprzestrzeni, w końcu pewne fakty oraz moje listy dotrą do oświeconych osób, a pastletter stanie się prawdziwym newsletterem.

To niezła wizytówka środowiska programistów Python. Profesjonalizm pełną gębą. A szczególnie kole w oczy to marne forum postawione na PHP-ie, które rozsyła hasła plain textem w mailu potwierdzającym rejestrację. Razem z rozwalającą się stroną PySilesia dumnie realizują cele statutowe polegające na promowaniu Pythona. Chyba Monty Pythona. Latającego cyrku.