From 2bb0586012124474094b4a2cbebd79cbac9c0c6f Mon Sep 17 00:00:00 2001 From: Allan Beaufour Date: Wed, 1 Jul 2026 06:27:44 -0400 Subject: [PATCH] Add Photo.getSafetyLevel() and int-convert safety_level (#173) Flickr has no flickr.photos.getSafetyLevel endpoint, but the safety level is returned by flickr.photos.getInfo (and by search with extras="safety_level"). Expose it conveniently: - Add a Photo.getSafetyLevel() helper that loads the photo if needed and returns the level as an int (0=none, 1=safe, 2=moderate, 3=restricted), matching the values setSafetyLevel accepts. - Register an int converter for safety_level so photo.safety_level is a consistent integer however the photo was fetched. Co-Authored-By: Claude Opus 4.8 (1M context) --- flickr_api/objects.py | 16 ++++++++++ test/test_photos.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/flickr_api/objects.py b/flickr_api/objects.py index 5148705..6ea4ee5 100644 --- a/flickr_api/objects.py +++ b/flickr_api/objects.py @@ -995,6 +995,7 @@ class Photo(FlickrObject): ), dict_converter(["posted", "lastupdate"], int), dict_converter(["views", "comments"], int), + dict_converter(["safety_level"], int), ] __display__ = ["id", "title"] __self_name__ = "photo_id" @@ -1182,6 +1183,21 @@ def format_result(r, token=None): return args, format_result + def getSafetyLevel(self) -> int: + """Return the safety level of the photo. + + Flickr does not provide a dedicated ``flickr.photos.getSafetyLevel`` + method, so the value is read from ``flickr.photos.getInfo`` (loading + the photo first if it has not been loaded yet). + + Returns an integer safety level: + ``0`` (none), ``1`` (safe), ``2`` (moderate) or ``3`` (restricted). + These match the values accepted by :meth:`setSafetyLevel`. + """ + if not hasattr(self, "safety_level"): + self.load() + return int(self.safety_level) + @caller("flickr.photos.getContactsPhotos") def getContactsPhotos(self, **args): def format_result(r, token=None): diff --git a/test/test_photos.py b/test/test_photos.py index 59dc1f2..9e4ce4d 100644 --- a/test/test_photos.py +++ b/test/test_photos.py @@ -464,6 +464,74 @@ def test_photo_get_info(self, mock_post): # Note: _content is renamed to text by clean_content self.assertEqual(photo.notes[0].text, "foo") + @patch.object(method_call.requests, "post") + def test_photo_get_info_safety_level(self, mock_post): + """Test that getInfo exposes safety_level as an int (issue #173)""" + json_response = { + "photo": { + "id": "2733", + "secret": "123456", + "server": "12", + "safety_level": "2", + "owner": {"nsid": "12037949754@N01"}, + "title": {"_content": "t"}, + "visibility": {"ispublic": 1, "isfriend": 0, "isfamily": 0}, + "dates": {"posted": "1100897479", "lastupdate": "1093022469"}, + "usage": {"candownload": 1}, + "publiceditability": {"cancomment": 1, "canaddmeta": 1}, + "tags": {"tag": []}, + "notes": {"note": []}, + } + } + mock_post.return_value = self._mock_response(json_response) + + photo = f.Photo(id="2733") + photo.getInfo() + + # safety_level is converted from the API string to an int + self.assertEqual(photo.safety_level, 2) + + @patch.object(method_call.requests, "post") + def test_photo_get_safety_level_loads(self, mock_post): + """Test Photo.getSafetyLevel loads the photo when needed (issue #173)""" + json_response = { + "photo": { + "id": "2733", + "secret": "123456", + "server": "12", + "safety_level": "3", + "owner": {"nsid": "12037949754@N01"}, + "title": {"_content": "t"}, + "visibility": {"ispublic": 1, "isfriend": 0, "isfamily": 0}, + "dates": {"posted": "1100897479", "lastupdate": "1093022469"}, + "usage": {"candownload": 1}, + "publiceditability": {"cancomment": 1, "canaddmeta": 1}, + "tags": {"tag": []}, + "notes": {"note": []}, + } + } + mock_post.return_value = self._mock_response(json_response) + + # Photo has not been loaded, so getSafetyLevel triggers getInfo + photo = f.Photo(id="2733") + level = photo.getSafetyLevel() + + self.assertEqual(level, 3) + self.assertIsInstance(level, int) + # getInfo was called exactly once to load the photo + self.assertEqual(mock_post.call_count, 1) + + @patch.object(method_call.requests, "post") + def test_photo_get_safety_level_already_loaded(self, mock_post): + """Test Photo.getSafetyLevel avoids a call when already known (issue #173)""" + # safety_level already present (e.g. from search extras="safety_level") + photo = f.Photo(id="2733", safety_level="1") + level = photo.getSafetyLevel() + + self.assertEqual(level, 1) + self.assertIsInstance(level, int) + # No API call needed + self.assertEqual(mock_post.call_count, 0) @patch.object(method_call.requests, "post") def test_person_get_not_in_set_photos(self, mock_post):