Merge lp:~benji/launchpad/bug-781600 into lp:launchpad
- bug-781600
- Merge into devel
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Benji York | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 13439 | ||||
Proposed branch: | lp:~benji/launchpad/bug-781600 | ||||
Merge into: | lp:launchpad | ||||
Diff against target: |
219 lines (+147/-5) 3 files modified
lib/lp/registry/interfaces/distribution.py (+24/-0) lib/lp/registry/model/distribution.py (+33/-2) lib/lp/registry/tests/test_distro_webservice.py (+90/-3) |
||||
To merge this branch: | bzr merge lp:~benji/launchpad/bug-781600 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | Approve | ||
Review via email: mp+67704@code.launchpad.net |
Commit message
[r=flacoste][bug=781600] Provide a web service API to retrieve the branch tip revisions (optionally since a given date) and official series information for a given distribution.
Description of the change
Bug 781600 is about providing a web service API to retrieve the branch
tip revisions (optionally since a given date) and official series
information for a given distribution. This branch adds a getBranchTips
named operation to distributions to provide that functionality.
Preimplemenation discussion was had with Francis.
The implementation is a relatively straight-forward SQL query who's
results are lightly post-processed to provide a slightly nicer (and
requested) structure for the return value.
Tests were added to lib/lp/
Specifically the TestGetBranchTips class.
Lint: some pre-existing lint fixed, now lint-free.
Benji York (benji) wrote : | # |
On Tue, Jul 12, 2011 at 3:34 PM, Francis J. Lacoste
<email address hidden> wrote:
> Review: Needs Fixing
Thanks very much for the review. I'm certainly not knowledgeable of
this area of LP yet and can use the help.
>> === modified file 'lib/lp/
>
>> @operation_
>> + since=Datetime(
>> + title=_("Time of last change"),
>> + description=_(
>> + "Return branches that have new tips since this timestamp."),
>> + required=False))
>> + @export_
>> + @export_
>> + @operation_
>> + def getBranchTips(
>> + """Return a collection of branches which have new tips since a date.
>> + """
>
> I'd suggest improving the docstring. Something along the lines of:
Looks good. Done.
> You probably need to make since=None since it's not a required parameter.
It seems odd to me to put the default in the interface in addition to in
the implementation. It has no effect and I worry that the DRY violation
could result in a loss of synchronization with the real default and
result in confusion. Also, the "required=False" bit is what indicates
that "since" is optional, the interface reader shouldn't care what
internal value we use to signify that it wasn't provided.
That being said, I'm only -0 so I'll add it given a bit of a push.
>> === modified file 'lib/lp/
>
>> """See `IBugTarget`."""
>> return get_bug_
>>
>> + def getBranchTips(self, since=None):
>> + """See `IDistribution`."""
>> + query = """
>> + SELECT unique_name, last_scanned_id,
>> + SeriesSourcePac
>> + JOIN DistroSeries ON Branch.distroseries = DistroSeries.id
>> + JOIN Distribution ON DistroSeries.
>> + JOIN SeriesSourcePac
>> + Branch.id = SeriesSourcePac
>> + WHERE Distribution.name = %s""" % sqlvalues(
>
> Not all branches will be official, so you want to use a LEFT OUTER JOIN on
> both SeriesSourcePac
I fixed that and added a test as mentioned below.
> Distro series id is pretty meangingless, we should return their name.
> DistroSeries.name instead of SeriesSourcePac
Done. I had to tweak the query more than just substituting
DistroSeries.name but not too much.
>> +
>> + if since is not None:
>> + query += (
>> + ' AND branch.last_scanned > %s' % sqlvalues(since))
>> +
>> + query += ' ORDER BY unique_name, last_scanned_id;'
>> +
>> + data = list(Store.
>> +
>> + # Group on location (unique_name) and revision (last_scanned_id).
>> + results = []
>> + for key, group in itertools.
>> + results.
>> + # Pull out all the official series IDs and append them as a list
>> + ...
Robert Collins (lifeless) wrote : | # |
On Thu, Jul 14, 2011 at 8:48 AM, Benji York <email address hidden> wrote:
>
>> It's kind of a shame that we have to materialize the whole results set here,
>> since for huge size, it will be batched anyway by the web service code. Would
>> it make sense to use an generator here? Maybe not, since we'd still retrieve
>> all results for later batches.
>
> Given that we use ORDER BY, I suspect the only savings would be not
> transmitting the non-batched results from the DB server to the app
> server. However, making the method a generator might still be a win
> because even though they will likely come back to get the rest, that
> request may well hit a different app server. (I don't think we do any
> sort of request affinity but would like to know if we do.)
>
> Later... Nope, that didn't work. lazr.restful exploded because it
> couldn't adapt the generator to IFiniteSequence. I would have expected
> returning a generator to work, but I can't find any other instances in
> the code base. I've left it as a non-generator for the time being but
> would appreciate any knowledge that can be shared on the topic.
So, order by does not on its own mean that the whole result set is
materialised; in fact, if there is an index that can satisfy the
ordering, and its limited, pg seems to prioritise it very highly.
It looks like the grouping could be done in the DB ?
If so then returning the result set may make sense.
Another alternative is to use the new batchnavigator 1.2.5 facilities
to do batch memos rather than slicing. However that may require some
glue into the lazr stuff.. OTOH you're pretty well placed for that
sort of surgery :)
-Rob
Francis J. Lacoste (flacoste) wrote : | # |
On 11-07-13 04:48 PM, Benji York wrote:
> On Tue, Jul 12, 2011 at 3:34 PM, Francis J. Lacoste
>
>> You probably need to make since=None since it's not a required parameter.
>
> It seems odd to me to put the default in the interface in addition to in
> the implementation. It has no effect and I worry that the DRY violation
> could result in a loss of synchronization with the real default and
> result in confusion. Also, the "required=False" bit is what indicates
> that "since" is optional, the interface reader shouldn't care what
> internal value we use to signify that it wasn't provided.
>
> That being said, I'm only -0 so I'll add it given a bit of a push.
Well, interfaces and implementation are usually not on the side of DRY
:-) To me, if the interface doesn't have a default parameter, it usually
means that client should expect to have to provide a parameter. Again,
the fact that this is mainly for webservice consumption makes this a
little less important. Anyway, Do as you will!
>
>>> === modified file 'lib/lp/
>>
>>> """See `IBugTarget`."""
>>> return get_bug_
>>>
>>> + def getBranchTips(self, since=None):
>>> + """See `IDistribution`."""
>>> + query = """
>>> + SELECT unique_name, last_scanned_id,
>>> + SeriesSourcePac
>>> + JOIN DistroSeries ON Branch.distroseries = DistroSeries.id
>>> + JOIN Distribution ON DistroSeries.
>>> + JOIN SeriesSourcePac
>>> + Branch.id = SeriesSourcePac
>>> + WHERE Distribution.name = %s""" % sqlvalues(
>>
>> Not all branches will be official, so you want to use a LEFT OUTER JOIN on
>> both SeriesSourcePac
>
> I fixed that and added a test as mentioned below.
>
>> Distro series id is pretty meangingless, we should return their name.
>> DistroSeries.name instead of SeriesSourcePac
>
> Done. I had to tweak the query more than just substituting
> DistroSeries.name but not too much.
Some comment on your new query:
> + query = """
> + SELECT unique_name, last_scanned_id, SPBDS.name FROM Branch
> + LEFT OUTER JOIN DistroSeries
> + ON Branch.distroseries = DistroSeries.id
You probably want a normal join here as you don't expect any branch here
without a distroseries. (And this join is just to get at the
distribution right?)
> + LEFT OUTER JOIN SeriesSourcePac
> + ON Branch.id = SeriesSourcePac
> + JOIN Distribution
> + ON DistroSeries.
> + LEFT OUTER JOIN DistroSeries SPBDS -- (SourcePackageB
> + ON SeriesSourcePac
> + WHERE Distribution.name = %s""" % sqlvalues(
You don't have to join the distribution here.
You could use
DistroSeries.
The SPBDDS left outer join seems right....
Benji York (benji) wrote : | # |
On Wed, Jul 13, 2011 at 6:13 PM, Francis J. Lacoste
<email address hidden> wrote:
> Review: Approve
> On 11-07-13 04:48 PM, Benji York wrote:
>> On Tue, Jul 12, 2011 at 3:34 PM, Francis J. Lacoste
> Some comment on your new query:
>
>> + query = """
>> + SELECT unique_name, last_scanned_id, SPBDS.name FROM Branch
>> + LEFT OUTER JOIN DistroSeries
>> + ON Branch.distroseries = DistroSeries.id
>
> You probably want a normal join here as you don't expect any branch here
> without a distroseries. (And this join is just to get at the
> distribution right?)
Good catch. Fixed.
>> + LEFT OUTER JOIN SeriesSourcePac
>> + ON Branch.id = SeriesSourcePac
>> + JOIN Distribution
>> + ON DistroSeries.
>> + LEFT OUTER JOIN DistroSeries SPBDS -- (SourcePackageB
>> + ON SeriesSourcePac
>> + WHERE Distribution.name = %s""" % sqlvalues(
>
> You don't have to join the distribution here.
>
> You could use
>
> DistroSeries.
Changed.
>>> It's kind of a shame that we have to materialize the whole results set here,
>>> since for huge size, it will be batched anyway by the web service code. Would
>>> it make sense to use an generator here? Maybe not, since we'd still retrieve
>>> all results for later batches.
>>
>> Given that we use ORDER BY, I suspect the only savings would be not
>> transmitting the non-batched results from the DB server to the app
>> server. However, making the method a generator might still be a win
>> because even though they will likely come back to get the rest, that
>> request may well hit a different app server. (I don't think we do any
>> sort of request affinity but would like to know if we do.)
>
> We don't and wouldn't really matter since each API request results in a
> different DB transaction.
Indeed. My history with ZODB is showing here. Where's the object cache
when you need it. ;)
>> Later... Nope, that didn't work. lazr.restful exploded because it
>> couldn't adapt the generator to IFiniteSequence. I would have expected
>> returning a generator to work, but I can't find any other instances in
>> the code base. I've left it as a non-generator for the time being but
>> would appreciate any knowledge that can be shared on the topic.
>
> Yeah, that's fine. The direction Robert suggested might work, but not
> sure how easy/hard it would be.
Given the number of WIP items I have at the moment I really need to move
on so I'm not going to investigate further.
>>>> === modified file 'lib/lp/
>>>
>
>>>
>>> You can use makeRelatedBran
>>> official source package. Or use
>>> makeRelatedBran
>>> make it official in one call.
>>
>> I tried to use makeRelatedBran
>> makeRelatedBranches but the need to have two different distro series
>> releases (series_1 and series_2 in the original cod...
Preview Diff
1 | === modified file 'lib/lp/registry/interfaces/distribution.py' | |||
2 | --- lib/lp/registry/interfaces/distribution.py 2011-06-16 21:17:37 +0000 | |||
3 | +++ lib/lp/registry/interfaces/distribution.py 2011-07-14 14:11:23 +0000 | |||
4 | @@ -28,6 +28,7 @@ | |||
5 | 28 | export_operation_as, | 28 | export_operation_as, |
6 | 29 | export_read_operation, | 29 | export_read_operation, |
7 | 30 | exported, | 30 | exported, |
8 | 31 | operation_for_version, | ||
9 | 31 | operation_parameters, | 32 | operation_parameters, |
10 | 32 | operation_returns_collection_of, | 33 | operation_returns_collection_of, |
11 | 33 | operation_returns_entry, | 34 | operation_returns_entry, |
12 | @@ -392,6 +393,29 @@ | |||
13 | 392 | """ | 393 | """ |
14 | 393 | 394 | ||
15 | 394 | @operation_parameters( | 395 | @operation_parameters( |
16 | 396 | since=Datetime( | ||
17 | 397 | title=_("Time of last change"), | ||
18 | 398 | description=_( | ||
19 | 399 | "Return branches that have new tips since this timestamp."), | ||
20 | 400 | required=False)) | ||
21 | 401 | @export_operation_as(name="getBranchTips") | ||
22 | 402 | @export_read_operation() | ||
23 | 403 | @operation_for_version('devel') | ||
24 | 404 | def getBranchTips(since): | ||
25 | 405 | """Return a list of branches which have new tips since a date. | ||
26 | 406 | |||
27 | 407 | Each branch information is a tuple of (branch_unique_name, | ||
28 | 408 | tip_revision, (official_series*)). | ||
29 | 409 | |||
30 | 410 | So for each branch in the distribution, you'll get the branch unique | ||
31 | 411 | name, the revision id of tip, and if the branch is official for some | ||
32 | 412 | series, the list of series name. | ||
33 | 413 | |||
34 | 414 | :param since: If specified, limits results to branches modified since | ||
35 | 415 | that date and time. | ||
36 | 416 | """ | ||
37 | 417 | |||
38 | 418 | @operation_parameters( | ||
39 | 395 | name=TextLine(title=_("Name"), required=True)) | 419 | name=TextLine(title=_("Name"), required=True)) |
40 | 396 | @operation_returns_entry(IDistributionMirror) | 420 | @operation_returns_entry(IDistributionMirror) |
41 | 397 | @export_read_operation() | 421 | @export_read_operation() |
42 | 398 | 422 | ||
43 | === modified file 'lib/lp/registry/model/distribution.py' | |||
44 | --- lib/lp/registry/model/distribution.py 2011-06-24 05:26:37 +0000 | |||
45 | +++ lib/lp/registry/model/distribution.py 2011-07-14 14:11:23 +0000 | |||
46 | @@ -10,7 +10,8 @@ | |||
47 | 10 | 'DistributionSet', | 10 | 'DistributionSet', |
48 | 11 | ] | 11 | ] |
49 | 12 | 12 | ||
51 | 13 | from operator import attrgetter | 13 | from operator import attrgetter, itemgetter |
52 | 14 | import itertools | ||
53 | 14 | 15 | ||
54 | 15 | from sqlobject import ( | 16 | from sqlobject import ( |
55 | 16 | BoolCol, | 17 | BoolCol, |
56 | @@ -654,6 +655,36 @@ | |||
57 | 654 | """See `IBugTarget`.""" | 655 | """See `IBugTarget`.""" |
58 | 655 | return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self)) | 656 | return get_bug_tags("BugTask.distribution = %s" % sqlvalues(self)) |
59 | 656 | 657 | ||
60 | 658 | def getBranchTips(self, since=None): | ||
61 | 659 | """See `IDistribution`.""" | ||
62 | 660 | query = """ | ||
63 | 661 | SELECT unique_name, last_scanned_id, SPBDS.name FROM Branch | ||
64 | 662 | JOIN DistroSeries | ||
65 | 663 | ON Branch.distroseries = DistroSeries.id | ||
66 | 664 | LEFT OUTER JOIN SeriesSourcePackageBranch | ||
67 | 665 | ON Branch.id = SeriesSourcePackageBranch.branch | ||
68 | 666 | LEFT OUTER JOIN DistroSeries SPBDS | ||
69 | 667 | -- (SPDBS stands for Source Package Branch Distro Series) | ||
70 | 668 | ON SeriesSourcePackageBranch.distroseries = SPBDS.id | ||
71 | 669 | WHERE DistroSeries.distribution = %s""" % sqlvalues(self.id) | ||
72 | 670 | |||
73 | 671 | if since is not None: | ||
74 | 672 | query += ( | ||
75 | 673 | ' AND branch.last_scanned > %s' % sqlvalues(since)) | ||
76 | 674 | |||
77 | 675 | query += ' ORDER BY unique_name, last_scanned_id;' | ||
78 | 676 | |||
79 | 677 | data = Store.of(self).execute(query) | ||
80 | 678 | |||
81 | 679 | result = [] | ||
82 | 680 | # Group on location (unique_name) and revision (last_scanned_id). | ||
83 | 681 | for key, group in itertools.groupby(data, itemgetter(0, 1)): | ||
84 | 682 | result.append(list(key)) | ||
85 | 683 | # Pull out all the official series names and append them as a list | ||
86 | 684 | # to the end of the current record, removing Nones from the list. | ||
87 | 685 | result[-1].append(filter(None, map(itemgetter(-1), group))) | ||
88 | 686 | return result | ||
89 | 687 | |||
90 | 657 | def getMirrorByName(self, name): | 688 | def getMirrorByName(self, name): |
91 | 658 | """See `IDistribution`.""" | 689 | """See `IDistribution`.""" |
92 | 659 | return Store.of(self).find( | 690 | return Store.of(self).find( |
93 | @@ -1422,7 +1453,7 @@ | |||
94 | 1422 | # the sourcepackagename from that. | 1453 | # the sourcepackagename from that. |
95 | 1423 | bpph = IStore(BinaryPackagePublishingHistory).find( | 1454 | bpph = IStore(BinaryPackagePublishingHistory).find( |
96 | 1424 | BinaryPackagePublishingHistory, | 1455 | BinaryPackagePublishingHistory, |
98 | 1425 | # See comment above for rationale for using an extra query | 1456 | # See comment above for rationale for using an extra query |
99 | 1426 | # instead of an inner join. (Bottom line, it would time out | 1457 | # instead of an inner join. (Bottom line, it would time out |
100 | 1427 | # otherwise.) | 1458 | # otherwise.) |
101 | 1428 | BinaryPackagePublishingHistory.archiveID.is_in( | 1459 | BinaryPackagePublishingHistory.archiveID.is_in( |
102 | 1429 | 1460 | ||
103 | === modified file 'lib/lp/registry/tests/test_distro_webservice.py' | |||
104 | --- lib/lp/registry/tests/test_distro_webservice.py 2011-03-23 16:28:51 +0000 | |||
105 | +++ lib/lp/registry/tests/test_distro_webservice.py 2011-07-14 14:11:23 +0000 | |||
106 | @@ -3,12 +3,21 @@ | |||
107 | 3 | 3 | ||
108 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
109 | 5 | 5 | ||
110 | 6 | from datetime import datetime | ||
111 | 7 | |||
112 | 8 | import pytz | ||
113 | 6 | from launchpadlib.errors import Unauthorized | 9 | from launchpadlib.errors import Unauthorized |
114 | 7 | 10 | ||
117 | 8 | from zope.security.management import endInteraction | 11 | from zope.security.management import ( |
118 | 9 | from zope.security.proxy import removeSecurityProxy | 12 | endInteraction, |
119 | 13 | newInteraction, | ||
120 | 14 | ) | ||
121 | 10 | 15 | ||
122 | 11 | from canonical.testing.layers import DatabaseFunctionalLayer | 16 | from canonical.testing.layers import DatabaseFunctionalLayer |
123 | 17 | from lp.code.model.seriessourcepackagebranch import ( | ||
124 | 18 | SeriesSourcePackageBranchSet, | ||
125 | 19 | ) | ||
126 | 20 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
127 | 12 | from lp.testing import ( | 21 | from lp.testing import ( |
128 | 13 | api_url, | 22 | api_url, |
129 | 14 | launchpadlib_for, | 23 | launchpadlib_for, |
130 | @@ -21,10 +30,88 @@ | |||
131 | 21 | 30 | ||
132 | 22 | layer = DatabaseFunctionalLayer | 31 | layer = DatabaseFunctionalLayer |
133 | 23 | 32 | ||
135 | 24 | def test_attempt_to_write_data_without_permission_gives_Unauthorized(self): | 33 | def test_write_without_permission_gives_Unauthorized(self): |
136 | 25 | distro = self.factory.makeDistribution() | 34 | distro = self.factory.makeDistribution() |
137 | 26 | endInteraction() | 35 | endInteraction() |
138 | 27 | lp = launchpadlib_for("anonymous-access") | 36 | lp = launchpadlib_for("anonymous-access") |
139 | 28 | lp_distro = lp.load(api_url(distro)) | 37 | lp_distro = lp.load(api_url(distro)) |
140 | 29 | lp_distro.active = False | 38 | lp_distro.active = False |
141 | 30 | self.assertRaises(Unauthorized, lp_distro.lp_save) | 39 | self.assertRaises(Unauthorized, lp_distro.lp_save) |
142 | 40 | |||
143 | 41 | |||
144 | 42 | class TestGetBranchTips(TestCaseWithFactory): | ||
145 | 43 | """Test the getBranchTips method and it's exposure to the web service.""" | ||
146 | 44 | |||
147 | 45 | layer = DatabaseFunctionalLayer | ||
148 | 46 | |||
149 | 47 | def setUp(self): | ||
150 | 48 | super(TestGetBranchTips, self).setUp() | ||
151 | 49 | self.distro = self.factory.makeDistribution() | ||
152 | 50 | series_1 = self.series_1 = self.factory.makeDistroRelease(self.distro) | ||
153 | 51 | series_2 = self.series_2 = self.factory.makeDistroRelease(self.distro) | ||
154 | 52 | source_package = self.factory.makeSourcePackage(distroseries=series_1) | ||
155 | 53 | branch = self.factory.makeBranch(sourcepackage=source_package) | ||
156 | 54 | unofficial_branch = self.factory.makeBranch(sourcepackage=source_package) | ||
157 | 55 | registrant = self.factory.makePerson() | ||
158 | 56 | now = datetime.now(pytz.UTC) | ||
159 | 57 | sourcepackagename = self.factory.makeSourcePackageName() | ||
160 | 58 | SeriesSourcePackageBranchSet.new( | ||
161 | 59 | series_1, PackagePublishingPocket.RELEASE, sourcepackagename, | ||
162 | 60 | branch, registrant, now) | ||
163 | 61 | SeriesSourcePackageBranchSet.new( | ||
164 | 62 | series_2, PackagePublishingPocket.RELEASE, sourcepackagename, | ||
165 | 63 | branch, registrant, now) | ||
166 | 64 | self.factory.makeRevisionsForBranch(branch) | ||
167 | 65 | self.branch_name = branch.unique_name | ||
168 | 66 | self.unofficial_branch_name = unofficial_branch.unique_name | ||
169 | 67 | self.branch_last_scanned_id = branch.last_scanned_id | ||
170 | 68 | endInteraction() | ||
171 | 69 | self.lp = launchpadlib_for("anonymous-access") | ||
172 | 70 | self.lp_distro = self.lp.distributions[self.distro.name] | ||
173 | 71 | |||
174 | 72 | def test_structure(self): | ||
175 | 73 | """The structure of the results is what we expect.""" | ||
176 | 74 | # The results should be structured as a list of | ||
177 | 75 | # (location, tip revision ID, [official series, official series, ...]) | ||
178 | 76 | item = self.lp_distro.getBranchTips()[0] | ||
179 | 77 | self.assertEqual(item[0], self.branch_name) | ||
180 | 78 | self.assertTrue(item[1], self.branch_last_scanned_id) | ||
181 | 79 | self.assertEqual( | ||
182 | 80 | sorted(item[2]), | ||
183 | 81 | [self.series_1.name, self.series_2.name]) | ||
184 | 82 | |||
185 | 83 | def test_same_results(self): | ||
186 | 84 | """Calling getBranchTips directly matches calling it via the API.""" | ||
187 | 85 | # The web service transmutes tuples into lists, so we have to do the | ||
188 | 86 | # same to the results of directly calling getBranchTips. | ||
189 | 87 | listified = [list(x) for x in self.distro.getBranchTips()] | ||
190 | 88 | self.assertEqual(listified, self.lp_distro.getBranchTips()) | ||
191 | 89 | |||
192 | 90 | def test_revisions(self): | ||
193 | 91 | """If a branch has revisions then the most recent one is returned.""" | ||
194 | 92 | revision = self.lp_distro.getBranchTips()[0][1] | ||
195 | 93 | self.assertNotEqual(None, revision) | ||
196 | 94 | |||
197 | 95 | def test_since(self): | ||
198 | 96 | """If "since" is given, return branches with new tips since then.""" | ||
199 | 97 | # There is at least one branch with a tip since the year 2000. | ||
200 | 98 | self.assertNotEqual(0, len(self.lp_distro.getBranchTips( | ||
201 | 99 | since=datetime(2000, 1, 1)))) | ||
202 | 100 | # There are no branches with a tip since the year 3000. | ||
203 | 101 | self.assertEqual(0, len(self.lp_distro.getBranchTips( | ||
204 | 102 | since=datetime(3000, 1, 1)))) | ||
205 | 103 | |||
206 | 104 | def test_series(self): | ||
207 | 105 | """The official series are included in the data.""" | ||
208 | 106 | actual_series_names = sorted([self.series_1.name, self.series_2.name]) | ||
209 | 107 | returned_series_names = sorted(self.lp_distro.getBranchTips()[0][-1]) | ||
210 | 108 | self.assertEqual(actual_series_names, returned_series_names) | ||
211 | 109 | |||
212 | 110 | def test_unofficial_branch(self): | ||
213 | 111 | """Not all branches are official.""" | ||
214 | 112 | # If a branch isn't official, the last skanned ID will be None and the | ||
215 | 113 | # official distro series list will be empty. | ||
216 | 114 | tips = self.lp_distro.getBranchTips()[1] | ||
217 | 115 | self.assertEqual(tips[0], self.unofficial_branch_name) | ||
218 | 116 | self.assertEqual(tips[1], None) | ||
219 | 117 | self.assertEqual(tips[2], []) |
> === modified file 'lib/lp/ registry/ interfaces/ distribution. py'
> @operation_ parameters( operation_ as(name= "getBranchTips" ) read_operation( ) for_version( 'devel' ) since):
> + since=Datetime(
> + title=_("Time of last change"),
> + description=_(
> + "Return branches that have new tips since this timestamp."),
> + required=False))
> + @export_
> + @export_
> + @operation_
> + def getBranchTips(
> + """Return a collection of branches which have new tips since a date.
> + """
I'd suggest improving the docstring. Something along the lines of:
Return a list of branches information which have new tips since a date.
Each branch information is a tuple of (branch_ unique_ name, tip_revision, series* )).
(official_
So for each branch in the distribution, you'll get the branch unique name,
the revision id of tip, and if the branch is official for some series, the
list of series name.
:param since: If specified, only returns branch modified since that date
time.
You probably need to make since=None since it's not a required parameter.
> === modified file 'lib/lp/ registry/ model/distribut ion.py'
> """See `IBugTarget`.""" tags("BugTask. distribution = %s" % sqlvalues(self)) kageBranch. distroseries FROM Branch distribution = Distribution.id kageBranch ON kageBranch. branch self.name)
> return get_bug_
>
> + def getBranchTips(self, since=None):
> + """See `IDistribution`."""
> + query = """
> + SELECT unique_name, last_scanned_id,
> + SeriesSourcePac
> + JOIN DistroSeries ON Branch.distroseries = DistroSeries.id
> + JOIN Distribution ON DistroSeries.
> + JOIN SeriesSourcePac
> + Branch.id = SeriesSourcePac
> + WHERE Distribution.name = %s""" % sqlvalues(
Not all branches will be official, so you want to use a LEFT OUTER JOIN on kageBranch and DistroSeries.
both SeriesSourcePac
Distro series id is pretty meangingless, we should return their name. kageBranch. distroseries
DistroSeries.name instead of SeriesSourcePac
> + of(self) .execute( query)) groupby( data, itemgetter(0, 1)): append( list(key) ) -1].append( map(itemgetter( -1), group))
> + if since is not None:
> + query += (
> + ' AND branch.last_scanned > %s' % sqlvalues(since))
> +
> + query += ' ORDER BY unique_name, last_scanned_id;'
> +
> + data = list(Store.
> +
> + # Group on location (unique_name) and revision (last_scanned_id).
> + results = []
> + for key, group in itertools.
> + results.
> + # Pull out all the official series IDs and append them as a list
> + # to the end of the current record.
> + results[
> + return results
> +
That last grouping will need to be modified to handle the NULL case.
It's kind of a shame that we have to materialize the whole results set here,
since for huge size, it will be batched anyway by the web service code. Would
it make sense to use an generator here? Maybe not, since we'd still retrieve
all results for later batche...