def get_data(self, request, context, *args, **kwargs): # Add data to the context here... restObj = RESTClient('admin', 'admin') restODLurl = 'http://172.17.78.59:8080/restconf/operational/network-topology:network-topology/' #response = restObj.get(restODLurl) context["jsonResponse"] = {'name': 'rakesh'} #response.content return context
def __init__(self, baseURL, username, password, headers={}): # normalize url url = absoluteURL(baseURL, '') if url.endswith('/'): url = url[:-1] self.url = url # set credentials self.username = username self.password = password self.requestHeaders = self.headers.copy() # instantiate and set REST client self.client = RESTClient() self.client.setCredentials(username, password) if isinstance(headers, dict): self.requestHeaders.update(headers) self.client.requestHeaders.update(self.requestHeaders)
class Basecamp(object): """Python wrapper for Basecamp API """ headers = {'Content-Type': 'application/xml', 'Accept': 'application/xml'} def __init__(self, baseURL, username, password, headers={}): # normalize url url = absoluteURL(baseURL, '') if url.endswith('/'): url = url[:-1] self.url = url # set credentials self.username = username self.password = password self.requestHeaders = self.headers.copy() # instantiate and set REST client self.client = RESTClient() self.client.setCredentials(username, password) if isinstance(headers, dict): self.requestHeaders.update(headers) self.client.requestHeaders.update(self.requestHeaders) # # Basecamp API # # Projects API Calls def getProjects(self): """Get projects REST: GET /projects.xml Returns all accessible projects. This includes active, inactive, and archived projects. """ path = '/projects.xml' rootElement = self.fromXML(self.get(path).contents) # TODO: use special Array attribute projects = [] for data in rootElement.getElementsByTagName('project'): projects.append(Project.load(data)) return projects def getProjectById(self, project_id): """Get project REST: GET /projects/#{project_id}.xml Returns a single project identified by its integer ID """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d.xml' % project_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Project with %d id is not found!' % project_id rootElement = self.fromXML(response.contents) return Project.load(rootElement) # To-do Lists API Calls def getTodoLists(self, responsible_party=None, company=False): """Get all lists (across projects) REST: GET /todo_lists.xml?responsible_party=#{id} Returns a list of todo-list records, with todo-item records that are assigned to the given "responsible party". If no responsible party is given, the current user is assumed to be the responsible party. The responsible party may be changed by setting the "responsible_party" query parameter to a blank string (for unassigned items), a person-id, or a company-id prefixed by a "c" (e.g., c1234). """ # make up a query string query = '' if responsible_party is not None: # ensure that we got numerical person or company id assert isinstance(responsible_party, int) query = '?responsible_party=' if company: query += 'c' query += str(int(responsible_party)) path = '/todo_lists.xml%s' % query response = self.get(path) if response.status == 404: if company: raise NotFoundError, 'Company with %d id is not found!' % responsible_party else: raise NotFoundError, 'Person with %d id is not found!' % responsible_party rootElement = self.fromXML(response.contents) # TODO: use special Array attribute todo_lists = [] for data in rootElement.getElementsByTagName('todo-list'): todo_lists.append(TodoList.load(data)) return todo_lists # To-do List Items API Calls def createTodoItem(self, todo_list_id, content, responsible_party=None, company=False, notify=False): """Create todo item (for a given todo list) REST: POST /todo_lists/#{todo_list_id}/todo_items.xml Creates a new todo item record for the given list. The new record begins its life in the "uncompleted" state. (See the "Complete" and "Uncomplete" actions.) It is added at the bottom of the given list. If a person is responsible for the item, give their id as the party_id value. If a company is responsible, prefix their company id with a 'c' and use that as the party_id value. If the item has a person as the responsible party, you can also use the "notify" key to indicate whether an email should be sent to that person to tell them about the assignment. XML Request: <todo-item> <content>#{content}</content> <!-- if the item has a responsible party --> <responsible-party>#{party_id}</responsible-party> <notify type="boolean">#{true|false}</notify> </todo-item> Response: Returns HTTP status code 201 (Created) on success, with the Location header being set to the URL for the new item. (The new item's integer ID may be extractd from that URL.) """ # ensure that we got numerical todo list id assert isinstance(todo_list_id, int) if company and notify: raise Exception, 'You can not nofity company!' path = '/todo_lists/%d/todo_items.xml' % todo_list_id # find author author = '' if responsible_party is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: responsible_party = person.id if responsible_party is not None: # ensure that we got numerical person or company id assert isinstance(responsible_party, int) author = '<responsible-party>%s%d</responsible-party>' % ( company and 'c' or '', responsible_party ) data = """ <todo-item> <content>%s</content> %s <notify type="boolean">%s</notify> </todo-item> """ % (content, author, notify and 'true' or 'false') response = self.post(path, data=data) # successfuly created entry if response.status == 201: id = int(dict(response.headers)['location'].split('/')[-1]) todo = TodoItem(id=id, content=content, responsible_party=responsible_party, responsible_party_type=((company and responsible_party) and 'c' or None), creator_id=responsible_party) return todo return self.getErrors(response.contents) def completeTodoItem(self, todo_item_id): """Complete item REST: PUT /todo_items/#{id}/complete.xml Marks the specified todo item as completed. Response: Returns HTTP status code 200 on success. """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/complete.xml' % todo_item_id response = self.put(path) # successfuly checked entry if response.status == 200: return True return self.getErrors(response.contents) def uncompleteTodoItem(self, todo_item_id): """Uncomplete item REST: PUT /todo_items/#{id}/uncomplete.xml If the specified todo item was previously marked as completed, this unmarks it, restoring it to an "uncompleted" state. If it was already in the uncompleted state, this call has no effect. Response: Returns HTTP status code 200 on success. """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/uncomplete.xml' % todo_item_id response = self.put(path) # successfuly checked entry if response.status == 200: return True return self.getErrors(response.contents) # Time Entries API Calls def getEntriesReport(self, _from, _to, subject_id=None, todo_item_id=None, filter_project_id=None, filter_company_id=None): """Get time report REST: GET /time_entries/report.xml Returns the set of time entries that match the given criteria. This action accepts the following query parameters: from, to, subject_id, todo_item_id, filter_project_id, and filter_company_id. Both from and to should be dates in YYYYMMDD format, and can be used to restrict the result to a particular date range. (No more than 6 months' worth of entries may be returned in a single query, though). The subject_id parameter lets you constrain the result to a single person's time entries. todo_item_id restricts the result to only those entries relating to the given todo item. filter_project_id restricts the entries to those for the given project, and filter_company_id restricts the entries to those for the given company. Response: <time-entries type="array"> <time-entry> ... </time-entry> ... </time-entries> """ # ensure that we got numerical ids query = ['from=%s' % _from, 'to=%s' % _to] if subject_id: assert isinstance(subject_id, int) query.append('subject_id=%d' % subject_id) if todo_item_id: assert isinstance(todo_item_id, int) query.append('todo_item_id=%d' % todo_item_id) if filter_project_id: assert isinstance(filter_project_id, int) query.append('filter_project_id=%d' % filter_project_id) if filter_company_id: assert isinstance(filter_company_id, int) query.append('filter_company_id=%d' % filter_company_id) path = '/time_entries/report.xml?%s' % '&'.join(query) response = self.get(path) if response.status != 200: return self.getErrors(response.contents) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute time_entries = [] for data in rootElement.getElementsByTagName('time-entry'): time_entries.append(TimeEntry.load(data)) return time_entries def getEntriesForTodoItem(self, todo_item_id): """Get all entries (for a todo item) REST: GET /todo_items/#{todo_item_id}/time_entries.xml Returns all time entries associated with the given todo item, in descending order by date. Response: <time-entries type="array"> <time-entry> ... </time-entry> ... </time-entries> """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/time_entries.xml' % todo_item_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Todo item with %d id is not found!' % \ todo_item_id rootElement = self.fromXML(response.contents) # TODO: use special Array attribute time_entries = [] for data in rootElement.getElementsByTagName('time-entry'): time_entries.append(TimeEntry.load(data)) return time_entries def createTimeEntryForTodoItem(self, todo_item_id, hours='', date=None, person_id=None, description=''): """Create entry (for a todo item) REST: POST /todo_items/#{todo_item_id}/time_entries.xml Creates a new time entry for the given todo item. XML Request: <time-entry> <person-id>#{person-id}</person-id> <date>#{date}</date> <hours>#{hours}</hours> <description>#{description}</description> </time-entry> Response: Returns HTTP status code 201 (Created) on success, with the Location header set to the URL of the new time entry. The integer ID of the entry may be extracted from that URL. """ # ensure that we got numerical todoitem id assert isinstance(todo_item_id, int) path = '/todo_items/%d/time_entries.xml' % todo_item_id # normalize all data for entry if date is None: # add current date date = time.strftime('%Y-%m-%d') # don't know how to get id of authenticated user if person_id is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: person_id = person.id entry = TimeEntry(person_id=person_id, todo_item_id=todo_item_id, date=date, hours=hours, description=description) response = self.post(path, data=entry.serialize()) # successfuly created entry if response.status == 201: entry.id = int(dict(response.headers)['location'].split('/')[-1]) return entry return self.getErrors(response.contents) def createTimeEntryForProject(self, project_id, hours='', date=None, person_id=None, description=''): """Create entry (for a given project) REST: POST /projects/#{project_id}/time_entries.xml Creates a new time entry for the given todo item. XML Request: <time-entry> <person-id>#{person-id}</person-id> <date>#{date}</date> <hours>#{hours}</hours> <description>#{description}</description> </time-entry> Response: Returns HTTP status code 201 (Created) on success, with the Location header set to the URL of the new time entry. The integer ID of the entry may be extracted from that URL. """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d/time_entries.xml' % project_id # normalize all data for entry if date is None: # add current date date = time.strftime('%Y-%m-%d') # don't know how to get id of authenticated user if person_id is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: person_id = person.id entry = TimeEntry(person_id=person_id, project_id=project_id, date=date, hours=hours, description=description) response = self.post(path, data=entry.serialize()) # successfuly created entry if response.status == 201: entry.id = int(dict(response.headers)['location'].split('/')[-1]) return entry return self.getErrors(response.contents) def destroyTimeEntry(self, id): """Destroy time entry REST: DELETE /time_entries/#{id}.xml Destroys the given time entry record. Response: Returns HTTP status code 200 on success. """ # ensure we got numerical entry id assert isinstance(id, int) path = '/time_entries/%d.xml' % id response = self.delete(path) if response.status == 200: return True elif response.status == 404: raise NotFoundError, 'Time Entry with <%d> id is not found!' % id else: return self.getErrors(response.contents) # Companies API Calls def getCompanies(self): """Get companies REST: GET /companies.xml Returns a list of all companies visible to the requesting user. Response: <companies> <company> ... </company> <company> ... </company> ... </company> </companies> """ path = '/companies.xml' rootElement = self.fromXML(self.get(path).contents) # TODO: use special Array attribute companies = [] for data in rootElement.getElementsByTagName('company'): companies.append(Company.load(data)) return companies def getCompaniesForProject(self, project_id): """Get companies on project REST: GET /projects/#{project_id}/companies.xml Returns a list of all companies associated with given project. Response: <companies> <company> ... </company> <company> ... </company> ... </company> </companies> """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d/companies.xml' response = self.get(path) if response.status == 404: raise NotFoundError, 'Project with %d id is not found!' % project_id rootElement = self.fromXML(response.contents) # TODO: use special Array attribute companies = [] for data in rootElement.getElementsByTagName('company'): companies.append(Company.load(data)) return companies def getCompanyById(self, company_id): """Get company REST: GET /companies/#{company_id}.xml Returns a single company identified by its integer ID. Response: <company> <id type="integer">1</id> <name>Globex Corporation</name> </company> """ # ensure that we got numerical company id assert isinstance(company_id, int) path = '/companies/%d.xml' % company_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Company with %d id is not found!' % company_id rootElement = self.fromXML(response.contents) return Company.load(rootElement) # People API Calls def getCurrentPerson(self): """Returns the currently logged in person (you) REST: GET /me.xml Response: <person> <id type="integer">#{id}</id> <user-name>#{user_name}</user-name> ... </person> """ path = '/me.xml' response = self.get(path) if response.status != 200: return self.getErrors(response.contents) data = self.fromXML(response.contents) return Person.load(data) def getPeopleForCompany(self, company_id, project_id=None): """Get people (for company) REST: GET /contacts/people/#{company_id} This will return all of the people in the given company. If a project id is given, it will be used to filter the set of people that are returned to include only those that can access the given project. Response: <people> <person> ... </person> <person> ... </person> ... </people> """ # ensure that we got numerical company id assert isinstance(company_id, int) path = '/contacts/people/%d' % company_id if project_id is not None: assert isinstance(project_id, int) path = '%s?project_id=%d' % (path, project_id) response = self.get(path) if response.status == 404: raise NotFoundError, 'Company [%d] or Project [%d] is not found!' % (company_id, project_id) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute people = [] for data in rootElement.getElementsByTagName('person'): people.append(Person.load(data)) return people def getPeopleForProject(self, project_id, company_id): """Get people on project REST: GET /projects/#{project_id}/contacts/people/#{company_id} This will return all of the people in the given company that can access the given project. Response: <people> <person> ... </person> <person> ... </person> ... </people> """ # ensure that we got numerical company and project ids assert isinstance(project_id, int) assert isinstance(company_id, int) path = '/projects/%d/contacts/people/%d' % (project_id, company_id) response = self.get(path) if response.status == 404: raise NotFoundError, 'Project [%d] or Company [%d] is not found!' % (project_id, company_id) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute people = [] for data in rootElement.getElementsByTagName('person'): people.append(Person.load(data)) return people def getPersonById(self, person_id): """Get person (by id) REST: GET /contacts/person/#{person_id} This will return information about the referenced person. Response: <person> ... </person> """ # ensure that we got numerical person id assert isinstance(person_id, int) path = '/contacts/person/%d' % person_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Person with %d id is not found!' % person_id rootElement = self.fromXML(response.contents) return Person.load(rootElement) def getPersonByLogin(self, login): """Finds person by it's login This method works only for persons from main company, not for persons from client companies. Return None if person wasn't found, and person if everything is fine. """ # ensure that we got correct input assert isinstance(login, str) # get owner companies, can-see-private property should not be set companies = [company for company in self.getCompanies() if company.can_see_private is None] if len(companies): people = self.getPeopleForCompany(companies[0].id) for person in people: if person.user_name is not None and person.user_name.lower() == login.lower(): return person return None def getAuthenticatedPerson(self): """Get authenticated person Basecamp API doesn't provide any methods to get authenticated user. That's why this method is implemented using a few implicit calls to Basecamp API. This works not so quickly as it is supposed to, but at the moment it seems like the only way to accomplish this task. Thus, firstly we get all companies and find the owner company among them. Then fetch people for that company and finally loop through all the people to find the one where the username matches the current login. We take only the owner company because only persons from the owner company (not client company) are allowed to retrieve people for a company. This means that for others this approach won't work. So, in case we got: 403 Error "The page you attempted to access is not accessible for clients", here is second way to get authenticated person. This approach is far from ideal. But as far as I know it is the only way to do this. Here are the detailed steps (http://www.basecamphq.com/forum-archive/viewtopic.php?id=2452): 1. Call /projects/list to get a list of projects. 2. Using the first project ID (it doesn't really matter which one it is), call /projects/#{project-id}/post_categories to get a category to post the message to. 3. Create a new message by calling /projects/#{project_id}/msg/create. (I use a GUID for the title of the message, but you could use anything you like.) 4. The reply you get back from /projects/#{project_id}/msg/create has two vital pieces of information. The author-id node gives you your person ID (where "you" are the person whose credentials were used by the API to login). The id node gives you the ID of the message, which you will use to ... 5. Call /msg/delete/#{id} using the message ID you just obtained. This will delete the message you created. """ # first way for company owners try: person = self.getPersonByLogin(self.username) except (UnauthorizedError, ForbiddenError), e: # person doesn't belong to owner's company pass else:
class Basecamp(object): """Python wrapper for Basecamp API """ headers = {'Content-Type': 'application/xml', 'Accept': 'application/xml'} def __init__(self, baseURL, username, password, headers={}): # normalize url url = absoluteURL(baseURL, '') if url.endswith('/'): url = url[:-1] self.url = url # set credentials self.username = username self.password = password self.requestHeaders = self.headers.copy() # instantiate and set REST client self.client = RESTClient() self.client.setCredentials(username, password) if isinstance(headers, dict): self.requestHeaders.update(headers) self.client.requestHeaders.update(self.requestHeaders) # # Basecamp API # # Projects API Calls def getProjects(self): """Get projects REST: GET /projects.xml Returns all accessible projects. This includes active, inactive, and archived projects. """ path = '/projects.xml' rootElement = self.fromXML(self.get(path).contents) # TODO: use special Array attribute projects = [] for data in rootElement.getElementsByTagName('project'): projects.append(Project.load(data)) return projects def getProjectById(self, project_id): """Get project REST: GET /projects/#{project_id}.xml Returns a single project identified by its integer ID """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d.xml' % project_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Project with %d id is not found!' % project_id rootElement = self.fromXML(response.contents) return Project.load(rootElement) # To-do Lists API Calls def getTodoLists(self, responsible_party=None, company=False): """Get all lists (across projects) REST: GET /todo_lists.xml?responsible_party=#{id} Returns a list of todo-list records, with todo-item records that are assigned to the given "responsible party". If no responsible party is given, the current user is assumed to be the responsible party. The responsible party may be changed by setting the "responsible_party" query parameter to a blank string (for unassigned items), a person-id, or a company-id prefixed by a "c" (e.g., c1234). """ # make up a query string query = '' if responsible_party is not None: # ensure that we got numerical person or company id assert isinstance(responsible_party, int) query = '?responsible_party=' if company: query += 'c' query += str(int(responsible_party)) path = '/todo_lists.xml%s' % query response = self.get(path) if response.status == 404: if company: raise NotFoundError, 'Company with %d id is not found!' % responsible_party else: raise NotFoundError, 'Person with %d id is not found!' % responsible_party rootElement = self.fromXML(response.contents) # TODO: use special Array attribute todo_lists = [] for data in rootElement.getElementsByTagName('todo-list'): todo_lists.append(TodoList.load(data)) return todo_lists # To-do List Items API Calls def createTodoItem(self, todo_list_id, content, responsible_party=None, company=False, notify=False): """Create todo item (for a given todo list) REST: POST /todo_lists/#{todo_list_id}/todo_items.xml Creates a new todo item record for the given list. The new record begins its life in the "uncompleted" state. (See the "Complete" and "Uncomplete" actions.) It is added at the bottom of the given list. If a person is responsible for the item, give their id as the party_id value. If a company is responsible, prefix their company id with a 'c' and use that as the party_id value. If the item has a person as the responsible party, you can also use the "notify" key to indicate whether an email should be sent to that person to tell them about the assignment. XML Request: <todo-item> <content>#{content}</content> <!-- if the item has a responsible party --> <responsible-party>#{party_id}</responsible-party> <notify type="boolean">#{true|false}</notify> </todo-item> Response: Returns HTTP status code 201 (Created) on success, with the Location header being set to the URL for the new item. (The new item's integer ID may be extractd from that URL.) """ # ensure that we got numerical todo list id assert isinstance(todo_list_id, int) if company and notify: raise Exception, 'You can not nofity company!' path = '/todo_lists/%d/todo_items.xml' % todo_list_id # find author author = '' if responsible_party is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: responsible_party = person.id if responsible_party is not None: # ensure that we got numerical person or company id assert isinstance(responsible_party, int) author = '<responsible-party>%s%d</responsible-party>' % ( company and 'c' or '', responsible_party) data = """ <todo-item> <content>%s</content> %s <notify type="boolean">%s</notify> </todo-item> """ % (content, author, notify and 'true' or 'false') response = self.post(path, data=data) # successfuly created entry if response.status == 201: id = int(dict(response.headers)['location'].split('/')[-1]) todo = TodoItem( id=id, content=content, responsible_party=responsible_party, responsible_party_type=((company and responsible_party) and 'c' or None), creator_id=responsible_party) return todo return self.getErrors(response.contents) def completeTodoItem(self, todo_item_id): """Complete item REST: PUT /todo_items/#{id}/complete.xml Marks the specified todo item as completed. Response: Returns HTTP status code 200 on success. """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/complete.xml' % todo_item_id response = self.put(path) # successfuly checked entry if response.status == 200: return True return self.getErrors(response.contents) def uncompleteTodoItem(self, todo_item_id): """Uncomplete item REST: PUT /todo_items/#{id}/uncomplete.xml If the specified todo item was previously marked as completed, this unmarks it, restoring it to an "uncompleted" state. If it was already in the uncompleted state, this call has no effect. Response: Returns HTTP status code 200 on success. """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/uncomplete.xml' % todo_item_id response = self.put(path) # successfuly checked entry if response.status == 200: return True return self.getErrors(response.contents) # Time Entries API Calls def getEntriesReport(self, _from, _to, subject_id=None, todo_item_id=None, filter_project_id=None, filter_company_id=None): """Get time report REST: GET /time_entries/report.xml Returns the set of time entries that match the given criteria. This action accepts the following query parameters: from, to, subject_id, todo_item_id, filter_project_id, and filter_company_id. Both from and to should be dates in YYYYMMDD format, and can be used to restrict the result to a particular date range. (No more than 6 months' worth of entries may be returned in a single query, though). The subject_id parameter lets you constrain the result to a single person's time entries. todo_item_id restricts the result to only those entries relating to the given todo item. filter_project_id restricts the entries to those for the given project, and filter_company_id restricts the entries to those for the given company. Response: <time-entries type="array"> <time-entry> ... </time-entry> ... </time-entries> """ # ensure that we got numerical ids query = ['from=%s' % _from, 'to=%s' % _to] if subject_id: assert isinstance(subject_id, int) query.append('subject_id=%d' % subject_id) if todo_item_id: assert isinstance(todo_item_id, int) query.append('todo_item_id=%d' % todo_item_id) if filter_project_id: assert isinstance(filter_project_id, int) query.append('filter_project_id=%d' % filter_project_id) if filter_company_id: assert isinstance(filter_company_id, int) query.append('filter_company_id=%d' % filter_company_id) path = '/time_entries/report.xml?%s' % '&'.join(query) response = self.get(path) if response.status != 200: return self.getErrors(response.contents) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute time_entries = [] for data in rootElement.getElementsByTagName('time-entry'): time_entries.append(TimeEntry.load(data)) return time_entries def getEntriesForTodoItem(self, todo_item_id): """Get all entries (for a todo item) REST: GET /todo_items/#{todo_item_id}/time_entries.xml Returns all time entries associated with the given todo item, in descending order by date. Response: <time-entries type="array"> <time-entry> ... </time-entry> ... </time-entries> """ # ensure that we got numerical id assert isinstance(todo_item_id, int) path = '/todo_items/%d/time_entries.xml' % todo_item_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Todo item with %d id is not found!' % \ todo_item_id rootElement = self.fromXML(response.contents) # TODO: use special Array attribute time_entries = [] for data in rootElement.getElementsByTagName('time-entry'): time_entries.append(TimeEntry.load(data)) return time_entries def createTimeEntryForTodoItem(self, todo_item_id, hours='', date=None, person_id=None, description=''): """Create entry (for a todo item) REST: POST /todo_items/#{todo_item_id}/time_entries.xml Creates a new time entry for the given todo item. XML Request: <time-entry> <person-id>#{person-id}</person-id> <date>#{date}</date> <hours>#{hours}</hours> <description>#{description}</description> </time-entry> Response: Returns HTTP status code 201 (Created) on success, with the Location header set to the URL of the new time entry. The integer ID of the entry may be extracted from that URL. """ # ensure that we got numerical todoitem id assert isinstance(todo_item_id, int) path = '/todo_items/%d/time_entries.xml' % todo_item_id # normalize all data for entry if date is None: # add current date date = time.strftime('%Y-%m-%d') # don't know how to get id of authenticated user if person_id is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: person_id = person.id entry = TimeEntry(person_id=person_id, todo_item_id=todo_item_id, date=date, hours=hours, description=description) response = self.post(path, data=entry.serialize()) # successfuly created entry if response.status == 201: entry.id = int(dict(response.headers)['location'].split('/')[-1]) return entry return self.getErrors(response.contents) def createTimeEntryForProject(self, project_id, hours='', date=None, person_id=None, description=''): """Create entry (for a given project) REST: POST /projects/#{project_id}/time_entries.xml Creates a new time entry for the given todo item. XML Request: <time-entry> <person-id>#{person-id}</person-id> <date>#{date}</date> <hours>#{hours}</hours> <description>#{description}</description> </time-entry> Response: Returns HTTP status code 201 (Created) on success, with the Location header set to the URL of the new time entry. The integer ID of the entry may be extracted from that URL. """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d/time_entries.xml' % project_id # normalize all data for entry if date is None: # add current date date = time.strftime('%Y-%m-%d') # don't know how to get id of authenticated user if person_id is None: # add authenticated user person = self.getAuthenticatedPerson() if person is not None: person_id = person.id entry = TimeEntry(person_id=person_id, project_id=project_id, date=date, hours=hours, description=description) response = self.post(path, data=entry.serialize()) # successfuly created entry if response.status == 201: entry.id = int(dict(response.headers)['location'].split('/')[-1]) return entry return self.getErrors(response.contents) def destroyTimeEntry(self, id): """Destroy time entry REST: DELETE /time_entries/#{id}.xml Destroys the given time entry record. Response: Returns HTTP status code 200 on success. """ # ensure we got numerical entry id assert isinstance(id, int) path = '/time_entries/%d.xml' % id response = self.delete(path) if response.status == 200: return True elif response.status == 404: raise NotFoundError, 'Time Entry with <%d> id is not found!' % id else: return self.getErrors(response.contents) # Companies API Calls def getCompanies(self): """Get companies REST: GET /companies.xml Returns a list of all companies visible to the requesting user. Response: <companies> <company> ... </company> <company> ... </company> ... </company> </companies> """ path = '/companies.xml' rootElement = self.fromXML(self.get(path).contents) # TODO: use special Array attribute companies = [] for data in rootElement.getElementsByTagName('company'): companies.append(Company.load(data)) return companies def getCompaniesForProject(self, project_id): """Get companies on project REST: GET /projects/#{project_id}/companies.xml Returns a list of all companies associated with given project. Response: <companies> <company> ... </company> <company> ... </company> ... </company> </companies> """ # ensure that we got numerical project id assert isinstance(project_id, int) path = '/projects/%d/companies.xml' response = self.get(path) if response.status == 404: raise NotFoundError, 'Project with %d id is not found!' % project_id rootElement = self.fromXML(response.contents) # TODO: use special Array attribute companies = [] for data in rootElement.getElementsByTagName('company'): companies.append(Company.load(data)) return companies def getCompanyById(self, company_id): """Get company REST: GET /companies/#{company_id}.xml Returns a single company identified by its integer ID. Response: <company> <id type="integer">1</id> <name>Globex Corporation</name> </company> """ # ensure that we got numerical company id assert isinstance(company_id, int) path = '/companies/%d.xml' % company_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Company with %d id is not found!' % company_id rootElement = self.fromXML(response.contents) return Company.load(rootElement) # People API Calls def getCurrentPerson(self): """Returns the currently logged in person (you) REST: GET /me.xml Response: <person> <id type="integer">#{id}</id> <user-name>#{user_name}</user-name> ... </person> """ path = '/me.xml' response = self.get(path) if response.status != 200: return self.getErrors(response.contents) data = self.fromXML(response.contents) return Person.load(data) def getPeopleForCompany(self, company_id, project_id=None): """Get people (for company) REST: GET /contacts/people/#{company_id} This will return all of the people in the given company. If a project id is given, it will be used to filter the set of people that are returned to include only those that can access the given project. Response: <people> <person> ... </person> <person> ... </person> ... </people> """ # ensure that we got numerical company id assert isinstance(company_id, int) path = '/contacts/people/%d' % company_id if project_id is not None: assert isinstance(project_id, int) path = '%s?project_id=%d' % (path, project_id) response = self.get(path) if response.status == 404: raise NotFoundError, 'Company [%d] or Project [%d] is not found!' % ( company_id, project_id) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute people = [] for data in rootElement.getElementsByTagName('person'): people.append(Person.load(data)) return people def getPeopleForProject(self, project_id, company_id): """Get people on project REST: GET /projects/#{project_id}/contacts/people/#{company_id} This will return all of the people in the given company that can access the given project. Response: <people> <person> ... </person> <person> ... </person> ... </people> """ # ensure that we got numerical company and project ids assert isinstance(project_id, int) assert isinstance(company_id, int) path = '/projects/%d/contacts/people/%d' % (project_id, company_id) response = self.get(path) if response.status == 404: raise NotFoundError, 'Project [%d] or Company [%d] is not found!' % ( project_id, company_id) rootElement = self.fromXML(response.contents) # TODO: use special Array attribute people = [] for data in rootElement.getElementsByTagName('person'): people.append(Person.load(data)) return people def getPersonById(self, person_id): """Get person (by id) REST: GET /contacts/person/#{person_id} This will return information about the referenced person. Response: <person> ... </person> """ # ensure that we got numerical person id assert isinstance(person_id, int) path = '/contacts/person/%d' % person_id response = self.get(path) if response.status == 404: raise NotFoundError, 'Person with %d id is not found!' % person_id rootElement = self.fromXML(response.contents) return Person.load(rootElement) def getPersonByLogin(self, login): """Finds person by it's login This method works only for persons from main company, not for persons from client companies. Return None if person wasn't found, and person if everything is fine. """ # ensure that we got correct input assert isinstance(login, str) # get owner companies, can-see-private property should not be set companies = [ company for company in self.getCompanies() if company.can_see_private is None ] if len(companies): people = self.getPeopleForCompany(companies[0].id) for person in people: if person.user_name is not None and person.user_name.lower( ) == login.lower(): return person return None def getAuthenticatedPerson(self): """Get authenticated person Basecamp API doesn't provide any methods to get authenticated user. That's why this method is implemented using a few implicit calls to Basecamp API. This works not so quickly as it is supposed to, but at the moment it seems like the only way to accomplish this task. Thus, firstly we get all companies and find the owner company among them. Then fetch people for that company and finally loop through all the people to find the one where the username matches the current login. We take only the owner company because only persons from the owner company (not client company) are allowed to retrieve people for a company. This means that for others this approach won't work. So, in case we got: 403 Error "The page you attempted to access is not accessible for clients", here is second way to get authenticated person. This approach is far from ideal. But as far as I know it is the only way to do this. Here are the detailed steps (http://www.basecamphq.com/forum-archive/viewtopic.php?id=2452): 1. Call /projects/list to get a list of projects. 2. Using the first project ID (it doesn't really matter which one it is), call /projects/#{project-id}/post_categories to get a category to post the message to. 3. Create a new message by calling /projects/#{project_id}/msg/create. (I use a GUID for the title of the message, but you could use anything you like.) 4. The reply you get back from /projects/#{project_id}/msg/create has two vital pieces of information. The author-id node gives you your person ID (where "you" are the person whose credentials were used by the API to login). The id node gives you the ID of the message, which you will use to ... 5. Call /msg/delete/#{id} using the message ID you just obtained. This will delete the message you created. """ # first way for company owners try: person = self.getPersonByLogin(self.username) except (UnauthorizedError, ForbiddenError), e: # person doesn't belong to owner's company pass else: