Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,23 @@ Post a log for trackable
tracking_code = "ABCDEF"
trackable.post_log(log, tracking_code)

Get geocaches by log type
---------------------------------------------------------------------------------------------------

.. code-block:: python

from pycaching.log import Type as LogType

for find in geocaching.my_finds(limit=5):
print(find.name)

for dnf in geocaching.my_dnfs(limit=2):
print(dnf.name)

for note in geocaching.my_logs(LogType.note, limit=6):
print(note.name)


Testing
===================================================================================================

Expand Down
41 changes: 41 additions & 0 deletions pycaching/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,47 @@ class Cache(object):
"log_page": "play/geocache/{wp}/log",
}

@classmethod
def _from_print_page(cls, geocaching, guid, soup):
"""Create a cache instance from a souped print-page and a GUID"""
if soup.find("p", "Warning") is not None:
raise errors.PMOnlyException()

cache_info = dict()
cache_info['guid'] = guid
cache_info['wp'] = soup.find(class_='HalfRight').find('h1').text.strip()
content = soup.find(id="Content")
cache_info['name'] = content.find("h2").text.strip()
cache_info['type'] = Type.from_filename(content.h2.img['src'].split('/')[-1].partition('.')[0])
cache_info['author'] = content.find(class_='Meta').text.partition(':')[2].strip()
diff_terr = content.find(class_='DiffTerr').find_all('img')
assert len(diff_terr) == 2
cache_info['difficulty'] = float(diff_terr[0]['alt'].split()[0])
cache_info['terrain'] = float(diff_terr[1]['alt'].split()[0])
cache_info['size'] = Size.from_string(content.find(class_='Third AlignCenter').p.img['alt'].partition(':')[2])
fav_text = content.find(class_='Third AlignRight').p.contents[2]
try:
cache_info['favorites'] = int(fav_text)
except ValueError: # element not present when 0 favorites
cache_info['favorites'] = 0
cache_info['hidden'] = parse_date(
content.find(class_='HalfRight AlignRight').p.text.strip().partition(':')[2].strip())
cache_info['location'] = Point.from_string(content.find(class_='LatLong').text.strip())
cache_info['state'] = None # not on the page
attributes = [img['src'].split('/')[-1].partition('.')[0].rpartition('-')
for img in content.find(class_='sortables').find_all('img')
if img.get('src') and img['src'].startswith('/images/attributes/')]
cache_info['attributes'] = {attr_name: attr_setting == 'yes'
for attr_name, _, attr_setting in attributes}
if 'attribute' in cache_info['attributes']: # 'blank' attribute
del cache_info['attributes']['attribute']
cache_info['summary'] = content.find("h2", text="Short Description").find_next("div").text
cache_info['description'] = content.find("h2", text="Long Description").find_next("div").text
hint = content.find(id='uxEncryptedHint')
cache_info['hint'] = hint.text.strip() if hint else None
cache_info['waypoints'] = Waypoint.from_html(content, table_id="Waypoints")
return Cache(geocaching, **cache_info)

def __init__(self, geocaching, wp, **kwargs):
"""Create a cache instance.

Expand Down
65 changes: 61 additions & 4 deletions pycaching/geocaching.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import json
import subprocess
import warnings
from urllib.parse import urljoin
from urllib.parse import parse_qs, urljoin, urlparse
from os import path
from pycaching.cache import Cache, Size
from pycaching.log import Log, Type as LogType
Expand All @@ -28,6 +28,7 @@ class Geocaching(object):
"login_page": "account/signin",
"search": "play/search",
"search_more": "play/search/more-results",
'my_logs': 'my/logs.aspx',
}
_credentials_file = ".gc_credentials"

Expand Down Expand Up @@ -340,12 +341,20 @@ def geocode(self, location):
"""
return Point.from_location(self, location)

def get_cache(self, wp):
"""Return a :class:`.Cache` object by its waypoint.
def get_cache(self, wp=None, guid=None):
"""Return a :class:`.Cache` object by its waypoint or GUID.

:param str wp: Cache waypoint.
:param str guid: Cache GUID.

.. note ::
Provide only the GUID or the waypoint, not both.
"""
return Cache(self, wp)
if (wp is None) == (guid is None):
raise TypeError('Please provide exactly one of `wp` or `guid`.')
if wp is not None:
return Cache(self, wp)
return self._cache_from_guid(guid)

def get_trackable(self, tid):
"""Return a :class:`.Trackable` object by its trackable ID.
Expand All @@ -367,3 +376,51 @@ def post_log(self, wp, text, type=LogType.found_it, date=None):
date = datetime.date.today()
l = Log(type=type, text=text, visited=date)
self.get_cache(wp).post_log(l)

def _cache_from_guid(self, guid):
logging.info('Loading cache with GUID {!r}'.format(guid))
print_page = self._request(Cache._urls["print_page"], params={"guid": guid})
return Cache._from_print_page(self, guid, print_page)

def my_logs(self, log_type=None, limit=float('inf')):
"""Get an iterable of the logged-in user's logs.

:param log_type: The log type to search for. Use a :class:`~.log.Type` value.
If set to ``None``, all logs will be returned (default: ``None``).
:param limit: The maximum number of results to return (default: infinity).
"""
logging.info("Getting {} of my logs of type {}".format(limit, log_type))
url = self._urls['my_logs']
if log_type is not None:
if isinstance(log_type, LogType):
log_type = log_type.value
url += '?lt={lt}'.format(lt=log_type)
cache_table = self._request(url).find(class_='Table')
if cache_table is None: # no finds on the account
return
cache_table = cache_table.tbody

yielded = 0
for row in cache_table.find_all('tr'):
link = row.find(class_='ImageLink')['href']
guid = parse_qs(urlparse(link).query)['guid'][0]

if yielded >= limit:
break

yield self.get_cache(guid=guid)
yielded += 1

def my_finds(self, limit=float('inf')):
"""Get an iterable of the logged-in user's finds.

:param limit: The maximum number of results to return (default: infinity).
"""
return self.my_logs(LogType.found_it, limit)

def my_dnfs(self, limit=float('inf')):
"""Get an iterable of the logged-in user's DNFs.

:param limit: The maximum number of results to return (default: infinity).
"""
return self.my_logs(LogType.didnt_find_it, limit)
1,454 changes: 1,454 additions & 0 deletions test/cassettes/geocaching_my_dnfs.json

Large diffs are not rendered by default.

1,454 changes: 1,454 additions & 0 deletions test/cassettes/geocaching_my_finds.json

Large diffs are not rendered by default.

74 changes: 74 additions & 0 deletions test/cassettes/geocaching_shortcut_getcache__by_guid.json

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@
def sanitize_cookies(interaction, cassette):
response = interaction.as_response()
response_cookies = response.cookies
request_body = response.request.body or '' # where secret values hide
# the or '' is necessary above because sometimes response.request.body
# is empty bytes, and that makes the later code complain.
request_cookies = dict()
for cookie in (interaction.as_response().request.headers.get('Cookie') or '').split('; '):
name, sep, val = cookie.partition('=')
if sep:
request_cookies[name] = val

secret_values = set()
for name in CLASSIFIED_COOKIES:
potential_val = response_cookies.get(name)
if potential_val:
secret_values.add(potential_val)

named_parameter_str = '&{}='.format(name)
if (named_parameter_str in request_body or
request_body.startswith(named_parameter_str[1:])):
i = request_body.index(name) + len(name) + 1 # +1 for the = sign
val = request_body[i:].split(',')[0] # after the comma is another cookie
secret_values.add(val)
potential_val = request_cookies.get(name)
if potential_val:
secret_values.add(potential_val)

for val in secret_values:
cassette.placeholders.append(
Expand Down
1 change: 1 addition & 0 deletions test/test_geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class TestTile(NetworkedTest):
POSITION_ACCURANCY = 3 # = up to 110 meters

def setUp(self):
super().setUp()
self.tile = Tile(self.gc, 8800, 5574, 14)

def test_download_utfgrid(self):
Expand Down
23 changes: 22 additions & 1 deletion test/test_geocaching.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,28 @@
from geopy.distance import great_circle

import pycaching
from pycaching import Geocaching, Point, Rectangle
from pycaching import Cache, Geocaching, Point, Rectangle
from pycaching.errors import NotLoggedInException, LoginFailedException, PMOnlyException
from . import username as _username, password as _password, NetworkedTest


class TestMethods(NetworkedTest):
def test_my_finds(self):
with self.recorder.use_cassette('geocaching_my_finds'):
finds = list(self.gc.my_finds(20))
self.assertEqual(20, len(finds))
for cache in finds:
self.assertTrue(cache.name)
self.assertTrue(isinstance(cache, Cache))

def test_my_dnfs(self):
with self.recorder.use_cassette('geocaching_my_dnfs'):
dnfs = list(self.gc.my_dnfs(20))
self.assertEqual(20, len(dnfs))
for cache in dnfs:
self.assertTrue(cache.name)
self.assertTrue(isinstance(cache, Cache))

def test_search(self):
with self.subTest("normal"):
tolerance = 2
Expand Down Expand Up @@ -280,6 +296,11 @@ def test_get_cache(self):
c = self.gc.get_cache("GC4808G")
self.assertEqual("Nekonecne ticho", c.name)

def test_get_cache__by_guid(self):
with self.recorder.use_cassette('geocaching_shortcut_getcache__by_guid'):
cache = self.gc.get_cache(guid='15ad3a3d-92c1-4f7c-b273-60937bcc2072')
self.assertEqual("Nekonecne ticho", cache.name)

def test_get_trackable(self):
with self.recorder.use_cassette('geocaching_shortcut_gettrackable'):
t = self.gc.get_trackable("TB1KEZ9")
Expand Down