Skip to content

Commit f82fb75

Browse files
authored
Merge pull request #113 from jarhill0/found-not-found-caches
Allow retrieving found/DNF caches
2 parents d1e42d7 + 60f7509 commit f82fb75

File tree

9 files changed

+3132
-14
lines changed

9 files changed

+3132
-14
lines changed

README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,23 @@ Post a log for trackable
219219
tracking_code = "ABCDEF"
220220
trackable.post_log(log, tracking_code)
221221
222+
Get geocaches by log type
223+
---------------------------------------------------------------------------------------------------
224+
225+
.. code-block:: python
226+
227+
from pycaching.log import Type as LogType
228+
229+
for find in geocaching.my_finds(limit=5):
230+
print(find.name)
231+
232+
for dnf in geocaching.my_dnfs(limit=2):
233+
print(dnf.name)
234+
235+
for note in geocaching.my_logs(LogType.note, limit=6):
236+
print(note.name)
237+
238+
222239
Testing
223240
===================================================================================================
224241

pycaching/cache.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,47 @@ class Cache(object):
110110
"log_page": "play/geocache/{wp}/log",
111111
}
112112

113+
@classmethod
114+
def _from_print_page(cls, geocaching, guid, soup):
115+
"""Create a cache instance from a souped print-page and a GUID"""
116+
if soup.find("p", "Warning") is not None:
117+
raise errors.PMOnlyException()
118+
119+
cache_info = dict()
120+
cache_info['guid'] = guid
121+
cache_info['wp'] = soup.find(class_='HalfRight').find('h1').text.strip()
122+
content = soup.find(id="Content")
123+
cache_info['name'] = content.find("h2").text.strip()
124+
cache_info['type'] = Type.from_filename(content.h2.img['src'].split('/')[-1].partition('.')[0])
125+
cache_info['author'] = content.find(class_='Meta').text.partition(':')[2].strip()
126+
diff_terr = content.find(class_='DiffTerr').find_all('img')
127+
assert len(diff_terr) == 2
128+
cache_info['difficulty'] = float(diff_terr[0]['alt'].split()[0])
129+
cache_info['terrain'] = float(diff_terr[1]['alt'].split()[0])
130+
cache_info['size'] = Size.from_string(content.find(class_='Third AlignCenter').p.img['alt'].partition(':')[2])
131+
fav_text = content.find(class_='Third AlignRight').p.contents[2]
132+
try:
133+
cache_info['favorites'] = int(fav_text)
134+
except ValueError: # element not present when 0 favorites
135+
cache_info['favorites'] = 0
136+
cache_info['hidden'] = parse_date(
137+
content.find(class_='HalfRight AlignRight').p.text.strip().partition(':')[2].strip())
138+
cache_info['location'] = Point.from_string(content.find(class_='LatLong').text.strip())
139+
cache_info['state'] = None # not on the page
140+
attributes = [img['src'].split('/')[-1].partition('.')[0].rpartition('-')
141+
for img in content.find(class_='sortables').find_all('img')
142+
if img.get('src') and img['src'].startswith('/images/attributes/')]
143+
cache_info['attributes'] = {attr_name: attr_setting == 'yes'
144+
for attr_name, _, attr_setting in attributes}
145+
if 'attribute' in cache_info['attributes']: # 'blank' attribute
146+
del cache_info['attributes']['attribute']
147+
cache_info['summary'] = content.find("h2", text="Short Description").find_next("div").text
148+
cache_info['description'] = content.find("h2", text="Long Description").find_next("div").text
149+
hint = content.find(id='uxEncryptedHint')
150+
cache_info['hint'] = hint.text.strip() if hint else None
151+
cache_info['waypoints'] = Waypoint.from_html(content, table_id="Waypoints")
152+
return Cache(geocaching, **cache_info)
153+
113154
def __init__(self, geocaching, wp, **kwargs):
114155
"""Create a cache instance.
115156

pycaching/geocaching.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import json
88
import subprocess
99
import warnings
10-
from urllib.parse import urljoin
10+
from urllib.parse import parse_qs, urljoin, urlparse
1111
from os import path
1212
from pycaching.cache import Cache, Size
1313
from pycaching.log import Log, Type as LogType
@@ -28,6 +28,7 @@ class Geocaching(object):
2828
"login_page": "account/signin",
2929
"search": "play/search",
3030
"search_more": "play/search/more-results",
31+
'my_logs': 'my/logs.aspx',
3132
}
3233
_credentials_file = ".gc_credentials"
3334

@@ -340,12 +341,20 @@ def geocode(self, location):
340341
"""
341342
return Point.from_location(self, location)
342343

343-
def get_cache(self, wp):
344-
"""Return a :class:`.Cache` object by its waypoint.
344+
def get_cache(self, wp=None, guid=None):
345+
"""Return a :class:`.Cache` object by its waypoint or GUID.
345346
346347
:param str wp: Cache waypoint.
348+
:param str guid: Cache GUID.
349+
350+
.. note ::
351+
Provide only the GUID or the waypoint, not both.
347352
"""
348-
return Cache(self, wp)
353+
if (wp is None) == (guid is None):
354+
raise TypeError('Please provide exactly one of `wp` or `guid`.')
355+
if wp is not None:
356+
return Cache(self, wp)
357+
return self._cache_from_guid(guid)
349358

350359
def get_trackable(self, tid):
351360
"""Return a :class:`.Trackable` object by its trackable ID.
@@ -367,3 +376,51 @@ def post_log(self, wp, text, type=LogType.found_it, date=None):
367376
date = datetime.date.today()
368377
l = Log(type=type, text=text, visited=date)
369378
self.get_cache(wp).post_log(l)
379+
380+
def _cache_from_guid(self, guid):
381+
logging.info('Loading cache with GUID {!r}'.format(guid))
382+
print_page = self._request(Cache._urls["print_page"], params={"guid": guid})
383+
return Cache._from_print_page(self, guid, print_page)
384+
385+
def my_logs(self, log_type=None, limit=float('inf')):
386+
"""Get an iterable of the logged-in user's logs.
387+
388+
:param log_type: The log type to search for. Use a :class:`~.log.Type` value.
389+
If set to ``None``, all logs will be returned (default: ``None``).
390+
:param limit: The maximum number of results to return (default: infinity).
391+
"""
392+
logging.info("Getting {} of my logs of type {}".format(limit, log_type))
393+
url = self._urls['my_logs']
394+
if log_type is not None:
395+
if isinstance(log_type, LogType):
396+
log_type = log_type.value
397+
url += '?lt={lt}'.format(lt=log_type)
398+
cache_table = self._request(url).find(class_='Table')
399+
if cache_table is None: # no finds on the account
400+
return
401+
cache_table = cache_table.tbody
402+
403+
yielded = 0
404+
for row in cache_table.find_all('tr'):
405+
link = row.find(class_='ImageLink')['href']
406+
guid = parse_qs(urlparse(link).query)['guid'][0]
407+
408+
if yielded >= limit:
409+
break
410+
411+
yield self.get_cache(guid=guid)
412+
yielded += 1
413+
414+
def my_finds(self, limit=float('inf')):
415+
"""Get an iterable of the logged-in user's finds.
416+
417+
:param limit: The maximum number of results to return (default: infinity).
418+
"""
419+
return self.my_logs(LogType.found_it, limit)
420+
421+
def my_dnfs(self, limit=float('inf')):
422+
"""Get an iterable of the logged-in user's DNFs.
423+
424+
:param limit: The maximum number of results to return (default: infinity).
425+
"""
426+
return self.my_logs(LogType.didnt_find_it, limit)

test/cassettes/geocaching_my_dnfs.json

Lines changed: 1454 additions & 0 deletions
Large diffs are not rendered by default.

test/cassettes/geocaching_my_finds.json

Lines changed: 1454 additions & 0 deletions
Large diffs are not rendered by default.

test/cassettes/geocaching_shortcut_getcache__by_guid.json

Lines changed: 74 additions & 0 deletions
Large diffs are not rendered by default.

test/helpers.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,21 @@
66
def sanitize_cookies(interaction, cassette):
77
response = interaction.as_response()
88
response_cookies = response.cookies
9-
request_body = response.request.body or '' # where secret values hide
10-
# the or '' is necessary above because sometimes response.request.body
11-
# is empty bytes, and that makes the later code complain.
9+
request_cookies = dict()
10+
for cookie in (interaction.as_response().request.headers.get('Cookie') or '').split('; '):
11+
name, sep, val = cookie.partition('=')
12+
if sep:
13+
request_cookies[name] = val
1214

1315
secret_values = set()
1416
for name in CLASSIFIED_COOKIES:
1517
potential_val = response_cookies.get(name)
1618
if potential_val:
1719
secret_values.add(potential_val)
1820

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

2625
for val in secret_values:
2726
cassette.placeholders.append(

test/test_geo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class TestTile(NetworkedTest):
182182
POSITION_ACCURANCY = 3 # = up to 110 meters
183183

184184
def setUp(self):
185+
super().setUp()
185186
self.tile = Tile(self.gc, 8800, 5574, 14)
186187

187188
def test_download_utfgrid(self):

test/test_geocaching.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,28 @@
1111
from geopy.distance import great_circle
1212

1313
import pycaching
14-
from pycaching import Geocaching, Point, Rectangle
14+
from pycaching import Cache, Geocaching, Point, Rectangle
1515
from pycaching.errors import NotLoggedInException, LoginFailedException, PMOnlyException
1616
from . import username as _username, password as _password, NetworkedTest
1717

1818

1919
class TestMethods(NetworkedTest):
20+
def test_my_finds(self):
21+
with self.recorder.use_cassette('geocaching_my_finds'):
22+
finds = list(self.gc.my_finds(20))
23+
self.assertEqual(20, len(finds))
24+
for cache in finds:
25+
self.assertTrue(cache.name)
26+
self.assertTrue(isinstance(cache, Cache))
27+
28+
def test_my_dnfs(self):
29+
with self.recorder.use_cassette('geocaching_my_dnfs'):
30+
dnfs = list(self.gc.my_dnfs(20))
31+
self.assertEqual(20, len(dnfs))
32+
for cache in dnfs:
33+
self.assertTrue(cache.name)
34+
self.assertTrue(isinstance(cache, Cache))
35+
2036
def test_search(self):
2137
with self.subTest("normal"):
2238
tolerance = 2
@@ -280,6 +296,11 @@ def test_get_cache(self):
280296
c = self.gc.get_cache("GC4808G")
281297
self.assertEqual("Nekonecne ticho", c.name)
282298

299+
def test_get_cache__by_guid(self):
300+
with self.recorder.use_cassette('geocaching_shortcut_getcache__by_guid'):
301+
cache = self.gc.get_cache(guid='15ad3a3d-92c1-4f7c-b273-60937bcc2072')
302+
self.assertEqual("Nekonecne ticho", cache.name)
303+
283304
def test_get_trackable(self):
284305
with self.recorder.use_cassette('geocaching_shortcut_gettrackable'):
285306
t = self.gc.get_trackable("TB1KEZ9")

0 commit comments

Comments
 (0)