연말이라 한동안 관심밖이었는데, 그새 TurboGears 2.0 Beta1이 release (그전에는 TurboGears 1.9.x 라 불리었었다) 되었다.

참고로, TurboGears 1.0은 python 2.5.x 에서만 설치된다. python 2.6 이상에서는 설치되지 않는다^^;;

연말 동안 opensuse에서 archlinux로 옮겨탔는데, default package로 python 2.6이 설치되어 있기 때문에,
이전 포스트는 모두 버리고(?), TurboGears 2.0으로 다시 포스팅해야겠다.
(2.0을 사용한지는 약 3달가까이 되어가는데, 개인적으로 1.0보다 2.0을 추천한다^^)
Posted by xHuro
,
#서론
TurboGears의 TGWebService는 현재 1.2.0까지 릴리즈된 상태이다. decorator를 이용하여 간단하게 WSDL을 자동으로 생성해주는 기능이 강력하지만(이전 포스트 참조), wsvalidate decorator를 사용할 때 Custom Objects에 대해서는 WSDL을 생성해주지 않는 문제가 발생한다. (편법으로 전혀 사용하지 않는 메소드를 하나 만들고, 그 메소드의 wsexpose에 Custom Objects 넣어주면 문제점을 피해갈 수 있으나, 정상적인 방법도 아닐 뿐더러 소스 코드도 깔끔해지지 않는다. 이전 포스트가 이런 편법이었다.)

이번 포스트에서는 Java와 연동에 앞서, 위의 WSValidate 문제점을 살펴보고, 커스터마이징해보도록 한다.

#필요한 Library
TGWebService(http://code.google.com/p/tgws/) : 사이트가 code google로 이사했다

#WsValidate의 문제점
TGWebService의 README 파일의 Custom Objects as Input 항목에서 설명하는데로 실행하면 WSDL에 반영되지 않는 문제점이 생긴다.

아래의 소스는 이전 포스트의 WebService 클래스를 조금 수정한 것이다. (wsexpose에는 아무것도 없고, wsvalidate에만 Custom Objects를 넣어줬다)

  @CodeGeass
from tgwebservices.controllers import WebServicesRoot, \
    WebServicesController, wsexpose, wsvalidate

from tgwebservices.runtime import typedproperty, unsigned
class Person(object):        
    name = ''
    id = ''

class CodeGeass(WebServicesRoot):
    @wsexpose()
    @wsvalidate(Person)
    def getPerson(self, value):
        person = value
        print person.id
        print person.name

이전 포스트와 동일하게 WSDL을 확인해보자.
http://localhost:8080/codegeass/soap/api.wsdl
   <wsdl:types>
     <xsd:schema elementFormDefault="qualified" targetNamespace="http://localhost:8080/codegeass/soap/types">
          <xsd:element name="getPerson">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="types:Person"/>
              </xsd:sequence>
            </xsd:complexType>

          </xsd:element>
          <xsd:element name="getPersonResponse">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="result" type="xsd:string"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
      </xsd:schema>

   </wsdl:types>
   <wsdl:message name="getPersonRequest" xmlns="http://localhost:8080/codegeass/soap/types">
       <wsdl:part name="parameters" element="types:getPerson"/>
   </wsdl:message>
   <wsdl:message name="getPersonResponse" xmlns="http://localhost:8080/codegeass/soap/types">
      <wsdl:part name="parameters" element="types:getPersonResponse"/>
   </wsdl:message>
... 후략
: 눈 씻고 찾아봐도, getPerson의 value type인 types:Data를 찾을 수 없다. 결국 이 WSDL을 사용하는 Client들은 types:Data에 대한 정보를 알 수 없게 되어, 이 메소드를 사용할 수 없게 된다.

#WSDL에 wsvalidate Decorator의 Custom Objects Type을 넣어주기 위한 해결책
편법적인 방법을 말고 좀더 근본적인 문제를 해결해 보도록 한다. TGWebService가 설치되어 있는 디렉토리를 둘러보면, controller.py가 있다. 이 안은 WebService 클래스안 각 메소드의 decorator 기능이 정의되어 있다.

  @wsexpose에 대한 정의
def wsexpose(return_type=basestring):
    """Exposes a method for access via web services. Only one value
    can be returned because many languages only support a single
    return value from methods.
    
    @param return_type the class that will be returned. This is used to
                       help statically typed clients."""
  ... 중략

        # a list denotes that the user is returning an array of the type
        # given by the first element in the array
        # we need to keep track of the complex types so that they can be
        # defined as appropriate for the given protocol
        if isinstance(return_type, list):
            rt = return_type[0]
            if rt not in primitives:
                register_complex_type(fi, rt)
        elif return_type not in primitives:
            register_complex_type(fi, return_type)

        return newfunc
    return entangle
: 볼드 표시된 부분을 자세히 살펴보자.
이전 포스트에서 언급하였듯이 wsexpose는 해당 메소드의 return_type을 설정하는 decorator이다. 볼드 표시한 부분은 return_type을 확인해서 알맞게 type을 등록하는 것을 알 수 있다.(return_type이 list인지, primitives인지, not primitives인지...)

  @wsvalidate에 대한 정의
def wsvalidate(*args, **kw):
    """Validates and converts incoming parameters. Also registers the
    parameters used by the method for use by statically typed languages.
    Method parameters can be specified via positional or keyword arguments.
    You should pass in the class used for each parameter."""
    def entangle(func):
        fi = register(func)
        input_types = dict()

        # match up the validators with the function parameters
        # the validators list doesn't include self, but the parameters list
        # does. So, the parameters list is offset by one higher.
        for i in range(0, len(args)):
            argtype = args[i]
            input_types[fi.params[i]] = argtype

        input_types.update(kw)
        fi.input_types = input_types
        return func
    return entangle
: wsvalidate는 위 wsexpose처럼 inpyt_type을 체크하고 type을 등록하는 부분이 없다. 그래서 아래와 같이 추가해준다. (wsexpose와 비슷하다.)

  @수정된 wsvalidate에 대한 정의
        # the validators list doesn't include self, but the parameters list
        # does. So, the parameters list is offset by one higher.
        for i in range(0, len(args)):
            argtype = args[i]
            input_types[fi.params[i]] = argtype

            if isinstance(argtype, list):
                rt = argtype[0]

                if rt not in primitives:
                    register_complex_type(fi, rt)
            elif argtype not in primitives:
                register_complex_type(fi, argtype)
 ... 후략

TurboGears를 멈추고, 리스타트하고 다시 WSDL을 확인해보자
<wsdl:types>
     <xsd:schema elementFormDefault="qualified" targetNamespace="http://localhost:8080/codegeass/soap/types">
        <xsd:complexType name="Person">
          <xsd:sequence>
            <xsd:element name="id" type="xsd:string"/>
            <xsd:element name="name" type="xsd:string"/>

          </xsd:sequence>
        </xsd:complexType>
          <xsd:element name="getPerson">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="types:Person"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="getPersonResponse">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="result" type="xsd:string"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
      </xsd:schema>
   </wsdl:types>
: 볼드 표시된 부분이 추가되었음을 알 수 있다.

#또 하나의 문제점...
이로써, 프로젝트 진행 중에 발생한 Custom Object는 해결이 되었으나, 또하나의 문제점이 발생하였다. wsvalidate에 List type([int], [float], [custom]...)을 적용하면 역시 WSDL에 반영되지 않는 것이다.

다음과 같이 array로 선언하고, WSDL을 확인해보자.
  @List type에 대한 CodeGeass
from tgwebservices.controllers import WebServicesRoot, \
    WebServicesController, wsexpose, wsvalidate

from tgwebservices.runtime import typedproperty, unsigned
class Person(object):        
    name = ''
    id = ''

class CodeGeass(WebServicesRoot):
    @wsexpose()
    @wsvalidate([Person])
    def getPerson(self, value):
        person = value
        print person.id
        print person.name

  @List type에 대한 WSDL
<wsdl:types>
     <xsd:schema elementFormDefault="qualified" targetNamespace="http://localhost:8080/codegeass/soap/types">
        <xsd:complexType name="Person">
          <xsd:sequence>
            <xsd:element name="id" type="xsd:string"/>
            <xsd:element name="name" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
          <xsd:element name="getPerson">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="types:Person_Array"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="getPersonResponse">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="result" type="xsd:string"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
      </xsd:schema>
   </wsdl:types>
: 도대체 types:Person_Array에 대한 정의는 어디에 숨었을까?

#WSDL에 wsvalidate Decorator의 List type 적용하기 위한 해결책
TGWebservice 소스중 soap.py를 분석해보자. 소스의 soapController 클래스는 일반 WebService 클래스의 wsexpose, wsvalidate Decorator의 분석 결과(위의 controller.py에서 체크한 결과)를 wsdl.html 템플릿으로 내뱉어주는 클래스다. 소스 코드의 마지막 부분에 있는 내부 메소드, _initialize_arrays 메소드를 잠시 들여다 보자.

  @soap.py 소스
...전략

    def _initialize_arrays(self):
        arrays = set()

        for func in self.wscontroller._ws_funcs.values():
            input_types = func._ws_func_info.input_types
            soap_type(func._ws_func_info.return_type, arrays)

        for cls in self.wscontroller._ws_complex_types:
            for key in ctvalues(cls):         
                soap_type(getattr(cls, key), arrays)
        self.arrays = arrays
: 일반 WebService 클래스(self.wscontroller)에 정의되어 있는 메소드의 return type을 arrays에 담는 작업(첫번째 loop)과 Custom Object type(두번째 2중 loop)을 arrays에 담는 작업을 이 _initialize_arrays에서 담당한다. 이 arrays는 이후 wsdl.html에서 loop를 돌면서 wsdl:type에 대해 출력한다.

이 메소드를 기준으로 소스를 분석해보면 return type(wsexpose)에 대해서만 처리할 뿐, input type(wsvalidate)에 대한 처리가 없음을 알 수 있다. 소스를 다음과 같이 추가하여 해당 메소드의 input type에 대한 처리하도록 한다.

  @수정된 soap.py 소스
    def _initialize_arrays(self):
        arrays = set()

        for func in self.wscontroller._ws_funcs.values():
            input_types = func._ws_func_info.input_types
            soap_type(func._ws_func_info.return_type, arrays)

            for it_key in input_types:        
                soap_type(input_types[it_key], arrays)

        for cls in self.wscontroller._ws_complex_types:
            for key in ctvalues(cls):         
                soap_type(getattr(cls, key), arrays)
        self.arrays = arrays

다시 한번 WSDL을 확인해보자
  @수정된 List type에 대한 WSDL
  <wsdl:types>
     <xsd:schema elementFormDefault="qualified" targetNamespace="http://localhost:8080/codegeass/soap/types">
        <xsd:complexType name="Person">
          <xsd:sequence>
            <xsd:element name="id" type="xsd:string"/><xsd:element name="name" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
          <xsd:element name="getPerson">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="types:Person_Array"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="getPersonResponse">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="result" type="xsd:string"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
        <xsd:complexType name="Person_Array">
          <xsd:sequence>
            <xsd:element maxOccurs="unbounded" name="item" nillable="true" type="types:Person"/>
          </xsd:sequence>
        </xsd:complexType>
      </xsd:schema>
   </wsdl:types>
: 전에 없었던 Person_Array에 대한 정의가 생겼음을 확인할 수 있다. Person_Array 안 element의 type 역시 Person으로 정의된 걸 알 수 있다.

#결론
지금까지 wsvalidate Decorator에 Custom Object나 List Type을 적용할 경우 WSDL에 반영되지 않는 문제점과 그 해결책을 확인해보았다. Python 자체가 Dynamic Typed Language라서 Input Type에 대한 체크를 하지 않는 것 같은데, 실질적으로 내가 참여한 프로젝트는 Java와 Python 사이를 WebService
로 연동하기 때문에, 수정전의 WSDL로는 Java로 클라이언트를 생성할 수가 없었다.

앞으로 TGWebServices가 버전업될 때 마다 패치하기 보다는, 반영되는 것을 기대하는 것이 좋을 듯 싶다.
Posted by xHuro
,
#서론
간만에 python과 TurboGears의 정리 포스트다. 대략 2달전쯤 끄적여놓은 건데, 이제서야 정리하게 되었다.
TurboGears의 TGWebService 라이브러리는 간단하게 WebService 메소드와 WSDL을 생성해준다.
ZSI는 TGWebService가 생성한 WSDL로 WebService Client를 위한 클래스들을 생성해준다.

이번 포스트에서는 TGWebService와 ZSI를 이용해서 간단한 WebService Server/Client를 생성해본다.
필요한 라이브러리는 다 설치되어 있다는 가정하에 진행하도록 한다.


#필요한 Python Library


#TurboGears의 새 프로젝트 생성하기
tg-admin quickstart CodeGeass


#WebService로 제공할 클래스와 메소드를 정의하기
from tgwebservices.controllers import WebServicesRoot, \
    WebServicesController, wsexpose, wsvalidate

from tgwebservices.runtime import typedproperty, unsigned
class Person(object):        
    name = ''
    id = ''

class CodeGeass(WebServicesRoot):
    @wsexpose(Person)
    @wsvalidate(Person)
    def getPerson(self, value):
        person = Person()
        person.id = value.name
        person.name = value.id

        return person

- @wsexpose는 getPerson이 반환하는 class의 type이다.
- @wsvalidate는 getPerson이 호출될때의 parameter type이다.
- type에는 built-in type(int, str 등)과 user-define class(단 object 상속해야한다)들이 포함될 수 있다.


#TurboGears의 root controller에 TGWebService 연결하기(controller.py에 하위 클래스 추가)
from turbogears import controllers, expose, flash
from codegeass import CodeGeass
# from model import *        
# import logging             
# log = logging.getLogger("codegeass.controllers")

class Root(controllers.RootController):
    codegeass = CodeGeass("http://localhost:8080/codegeass/")

    @expose(template="codegeass.templates.welcome")
    def index(self):
        import time          
        # log.debug("Happy TurboGears Controller Responding For Duty")
        flash("Your application is now running")
        return dict(now=time.ctime())

- codegeass = .... 이 부분이 앞에서 만들어놓은 CodeGeass class를 root controller와 연결하는 구문이다. 생성자의 parameter는  CodeGeass의 instance가  web상에서 호출되는 URI를 다 넣어준다. (http://localhost:8080/codegeass/ 에서 codegeass/가 해당된다. 이해 잘 안된다면 TurboGears의 controller에 대해 구글링해서 공부하는 것이 좋다)


#TurboGears 서버 구동하기
./start-codegeass.py


#TGWebService로 생성된 WSDL 확인하기
http://localhost:8080/codegeass/soap/api.wsdl

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="CodeGeass" xmlns:types="http://localhost:8080/codegeass/soap/types" xmlns:soapenc="http://www.w3.org/2001/09/soap-encoding" targetNamespace="http://localhost:8080/codegeass/soap/" xmlns:tns="http://localhost:8080/codegeass/soap/">
   <wsdl:types>
     <xsd:schema elementFormDefault="qualified" targetNamespace="http://localhost:8080/codegeass/soap/types">
        <xsd:complexType name="Person">
          <xsd:sequence>
            <xsd:element name="id" type="xsd:string"/><xsd:element name="name" type="xsd:string"/>
          </xsd:sequence>
        </xsd:complexType>
          <xsd:element name="getPerson">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="types:Person"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="getPersonResponse">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="result" type="types:Person"/>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
      </xsd:schema>
   </wsdl:types>

   <wsdl:message name="getPersonRequest" xmlns="http://localhost:8080/codegeass/soap/types">
       <wsdl:part name="parameters" element="types:getPerson"/>
   </wsdl:message>

   <wsdl:message name="getPersonResponse" xmlns="http://localhost:8080/codegeass/soap/types">
      <wsdl:part name="parameters" element="types:getPersonResponse"/>
   </wsdl:message>
   <wsdl:portType name="CodeGeass_PortType">
      <wsdl:operation name="getPerson">
         <wsdl:input message="tns:getPersonRequest"/>
         <wsdl:output message="tns:getPersonResponse"/>
      </wsdl:operation>
   </wsdl:portType>

   <wsdl:binding name="CodeGeass_Binding" type="tns:CodeGeass_PortType">
      <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
      <wsdl:operation name="getPerson">
         <soap:operation soapAction="getPerson"/>
         <wsdl:input>
            <soap:body use="literal"/>
         </wsdl:input>
         <wsdl:output>
            <soap:body use="literal"/>
         </wsdl:output>
      </wsdl:operation>
   </wsdl:binding>

   <wsdl:service name="CodeGeass">
      <wsdl:documentation>WSDL File for CodeGeass</wsdl:documentation>
      <wsdl:port binding="tns:CodeGeass_Binding" name="CodeGeass_PortType">
         <soap:address location="http://localhost:8080/codegeass/soap/"/>
      </wsdl:port>
   </wsdl:service>
</wsdl:definitions>

- xml로 정의된 Person, 그리고 getPerson을 볼 수 있다.
- 몇개의 URL이 더 있는데, TGWebServices의 README.txt의 URLs를 참고하면 된다.


#ZSI로 WebService client를 위한 코드 생성하기
wsdl2py --url http://localhost:8080/codegeass/soap/api.wsdl

-WSDL을 이용해서 코드를 생성한다.(Code Generation from WSDL and XML Schema 참고) 위의 명령을 실행하면 다음과 같은 python 파일이 생성된다.
CodeGeass_services.py : TGWebServices에서의 portType, Locator 등의 Class가 있는 파일
CodeGeass_services_types.py : TGWebServices에서 정의된 Data Type이 정의된 Class가 있는 파일  


#생성된 코드를 이용하여 WebService Client 만들기
from CodeGeass_services import *
from CodeGeass_services_types import ns0

loc = CodeGeassLocator()
port = loc.getCodeGeass_PortType()
req = getPersonRequest()

inputMessage = ns0.Person_Def(None, None)
inputMessage._id = "dada"
inputMessage._name = "lulush"

req._value = inputMessage
res = port.getPerson(req)

returnMessage = res._result
print "Result ID:", returnMessage._id
print "Result name:", returnMessage._name

Locator와 Port를 설정하고, 호출할 메소드의 Request(getPerson+Request)에 파라메터를 넣는다.
그리고 Port를 통해 호출하면 값을 받아올 수 있다.
req._value 와 res._result는 위의 WSDL의 <xsd:element name="getPerson">, <xsd:element name="getPersonResponse">에서 하위 노드의 value 값과 매칭된다.


#실행

위의 소스 코드를 실행하면, inputMessage의 _id와 _name이 swap되어 나오면 정상적으로 처리된 것이다.


#결론
TGWebService를 사용해서 간단히 WSDL을 만들고, 그 WSDL을 ZSI로 코드 생성하고 클라이언트 만드는 방법을 알아보았다. 여기서는 TGWebService의 WSDL을 사용했는데, 조금 응용하면 자바, php등의 WSDL을 가지고 Client를 만들 수 있으며, 역으로 다른 시스템에서 TGWebService의 WSDL로 Client를 생성할 수도 있다.
다음 포스팅은 Java의 XFire와 TGWebService의 연동 방법에 대해 알아본다.
Posted by xHuro
,
TurboGears에서 SQLObject를 이용한 Unit Test시 dburi을 설정하지 않으면, 기본적으로 sqlite(memory DB)로 설정된다. Unit Test시에는 아래와 같이 초기에 설정해준다.

from turbogears import testutil, database
database.set_db_uri("db2://test:test@dada.pe.kr")


여기서 db2 대신, 각각 쓰고 있는 DB로 설정하면된다.
Posted by xHuro
,