Skip to content

Commit

Permalink
Implement Webdx Feature IDs datastore and endpoint (#4722)
Browse files Browse the repository at this point in the history
* Complete api and datastore implementation

* Address comments
  • Loading branch information
KyleJu authored Jan 24, 2025
1 parent 871b13d commit 396aa9c
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 5 deletions.
46 changes: 46 additions & 0 deletions api/webdx_feature_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2025 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from collections import OrderedDict

from framework import basehandlers
from internals.webdx_feature_models import WebdxFeatures

MISSING_FEATURE_ID = 'N/A'
TBD_FEATURE_ID = 'TBD'


class WebdxFeatureAPI(basehandlers.APIHandler):
"""The list of ordered webdx feature ID populates the "Web Feature ID" select field
in the guide form"""

def do_get(self, **kwargs):
"""Returns an ordered dict with Webdx feature id as both keys and values."""
webdx_features = WebdxFeatures.get_webdx_feature_id_list()
if not webdx_features:
logging.error('Webdx feature id list is empty.')
return {}

feature_ids_dict = OrderedDict()
# The first key, value pair is the id when features are missing from the list.
feature_ids_dict[MISSING_FEATURE_ID] = [MISSING_FEATURE_ID, MISSING_FEATURE_ID]
feature_ids_dict[TBD_FEATURE_ID] = [TBD_FEATURE_ID, TBD_FEATURE_ID]

feature_list = webdx_features.feature_ids
feature_list.sort()
for id in feature_list:
feature_ids_dict[id] = [id, id]

return feature_ids_dict
61 changes: 61 additions & 0 deletions api/webdx_feature_api_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2025 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import testing_config
from collections import OrderedDict

import flask

from api.webdx_feature_api import WebdxFeatureAPI
from internals.webdx_feature_models import WebdxFeatures

test_app = flask.Flask(__name__)

class WebdxFeatureAPITest(testing_config.CustomTestCase):

def setUp(self):
self.webdx_features = WebdxFeatures(feature_ids = ['zzz', 'aaa'])
self.webdx_features.put()

self.handler = WebdxFeatureAPI()
self.request_path = '/api/v0/webdxfeatures'

def tearDown(self):
for entity in WebdxFeatures.query():
entity.key.delete()

def test_do_get__success(self):
testing_config.sign_out()

with test_app.test_request_context(self.request_path):
actual = self.handler.do_get()

expected = OrderedDict(
[
('N/A', ['N/A', 'N/A']),
('TBD', ['TBD', 'TBD']),
('aaa', ['aaa', 'aaa']),
('zzz', ['zzz', 'zzz']),
]
)
self.assertEqual(actual, expected)

def test_do_get__empty_data(self):
testing_config.sign_out()
self.webdx_features.key.delete()

with test_app.test_request_context(self.request_path):
actual = self.handler.do_get()

self.assertEqual(actual, {})
4 changes: 2 additions & 2 deletions internals/maintenance_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from internals.core_enums import *
from internals.feature_links import batch_index_feature_entries
from internals import stage_helpers
from internals.webdx_feature_models import WebdxFeatures
from webstatus_openapi import ApiClient, DefaultApi, Configuration, ApiException, Feature
import settings

Expand Down Expand Up @@ -832,9 +833,8 @@ def get_template_data(self, **kwargs) -> str:
)
return 'Running FetchWebdxFeatureId() job failed.'

# TODO(kyleju): save it to datastore.
feature_ids_list = [feature_data.feature_id for feature_data in all_data_list]
feature_ids_list.sort()
WebdxFeatures.store_webdx_feature_id_list(feature_ids_list)
return (f'{len(feature_ids_list)} feature ids are successfully stored.')


Expand Down
13 changes: 10 additions & 3 deletions internals/maintenance_scripts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from internals.core_models import FeatureEntry, Stage, MilestoneSet
from internals.review_models import Gate, Vote
from internals import stage_helpers
from internals.webdx_feature_models import WebdxFeatures
from webstatus_openapi import FeaturePage, ApiException
import settings

Expand Down Expand Up @@ -864,10 +865,12 @@ class FetchWebdxFeatureIdTest(testing_config.CustomTestCase):

def setUp(self):
self.handler = maintenance_scripts.FetchWebdxFeatureId()
# TODO(kyleju): set up and clean up datastore.
self.webdx_features = WebdxFeatures(feature_ids = ['test1'])
self.webdx_features.put()

def tearDown(self):
return
for entity in WebdxFeatures.query():
entity.key.delete()

@mock.patch('webstatus_openapi.DefaultApi.list_features')
def test_fetch_webdx_feature_ids__success(self, mock_list_features):
Expand All @@ -881,7 +884,7 @@ def test_fetch_webdx_feature_ids__success(self, mock_list_features):
'firefox': {'date': '2008-06-17', 'status': 'available', 'version': '3'},
'safari': {'date': '2023-03-27', 'status': 'available', 'version': '16.4'},
},
'feature_id': 'font-size-adjust',
'feature_id': 'foo',
'name': 'font-size-adjust',
'spec': {
'links': [
Expand Down Expand Up @@ -919,6 +922,10 @@ def test_fetch_webdx_feature_ids__success(self, mock_list_features):
result = self.handler.get_template_data()

self.assertEqual('2 feature ids are successfully stored.', result)
expected = WebdxFeatures.get_by_id(self.webdx_features.key.integer_id())
self.assertEqual(len(expected.feature_ids), 2)
self.assertEqual(expected.feature_ids[0], 'foo')
self.assertEqual(expected.feature_ids[1], 'bar')

@mock.patch('webstatus_openapi.DefaultApi.list_features')
def test_fetch_webdx_feature_ids__exceptions(self, mock_list_features):
Expand Down
37 changes: 37 additions & 0 deletions internals/webdx_feature_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2025 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.cloud import ndb # type: ignore


class WebdxFeatures(ndb.Model):
"""A singleton model to store Webdx feature IDs"""
feature_ids = ndb.StringProperty(repeated=True)

@classmethod
def get_webdx_feature_id_list(cls):
fetch_results = cls.query().fetch(1)
if not fetch_results:
return None

return fetch_results[0]

@classmethod
def store_webdx_feature_id_list(cls, new_list: list[str]):
webdx_features = WebdxFeatures.get_webdx_feature_id_list()
if not webdx_features:
webdx_features = WebdxFeatures(feature_ids=new_list)
else:
webdx_features.feature_ids = new_list
webdx_features.put()
51 changes: 51 additions & 0 deletions internals/webdx_feature_models_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2025 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import testing_config

from internals.webdx_feature_models import WebdxFeatures


class WebdxFeaturesTest(testing_config.CustomTestCase):
def setUp(self):
self.webdx = WebdxFeatures(feature_ids=['abc'])
self.webdx.put()

def tearDown(self):
self.webdx.key.delete()

def test_get_webdx_feature_id_list(self):
result = WebdxFeatures.get_webdx_feature_id_list()

self.assertIsNotNone(result)
self.assertEqual(len(result.feature_ids), 1)
self.assertEqual(result.feature_ids[0], 'abc')

def test_store_webdx_feature_id_list__success(self):
WebdxFeatures.store_webdx_feature_id_list(['foo'])

result = WebdxFeatures.query().fetch()
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].feature_ids[0], 'foo')

def test_store_webdx_feature_id_list__success_from_empty(self):
self.webdx.key.delete()

WebdxFeatures.store_webdx_feature_id_list(['foo'])

result = WebdxFeatures.query().fetch()
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].feature_ids[0], 'foo')
3 changes: 3 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
stages_api,
stars_api,
token_refresh_api,
webdx_feature_api,
)
from framework import basehandlers, csp, sendemail
from internals import (
Expand Down Expand Up @@ -177,6 +178,8 @@ class Route:
origin_trials_api.OriginTrialsAPI),
Route(f'{API_BASE}/origintrials/<int:feature_id>/<int:extension_stage_id>/extend',
origin_trials_api.OriginTrialsAPI),

Route(f'{API_BASE}/webdxfeatures', webdx_feature_api.WebdxFeatureAPI),
]

# The Routes below that have no handler specified use SPAHandler.
Expand Down

0 comments on commit 396aa9c

Please sign in to comment.